editor/monaco/editor_monaco.js

/**
 *  MonacoEditor
 *  Wrapper for using the Monaco editor in Codestrates
 * 
 *  Copyright 2020, 2021 Rolf Bagge, Janus B. Kristensen, CAVI,
 *  Center for Advanced Visualization and Interaction, Aarhus University
 *    
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0

 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
**/

/* global webstrate, cQuery, Editor, monaco, EditorManager */

wpm.onRemoved(()=>{
    EditorManager.unregisterEditor(MonacoEditor, "monaco-editor");
});

/**
 * An editor implementation using Monaco
 *
 * @memberof Editors
 * @extends Editors.Editor
 */
class MonacoEditor extends Editor {
    constructor(fragment, options = {}) {
        super("monaco-editor", fragment, options);

        this.options = options;

        this.foreignDecorators = new Map();

        this.setupEditor();
    }

    static registerExtraType(mimeType, language) {
        MonacoEditor.extraTypes.set(mimeType, language);
        EditorManager.registerEditorType(mimeType, this);
    }

    async setupEditor() {
        let self = this;

        let language = "text";

        switch (self.fragment.type) {
            case "model/vnd.usda":
            case "text/x-latex":
                language = "plaintext";
                break;

            case "text/javascript":
            case "text/javascript+babel":
            case "text/whenjs":
            case "text/p5js":
                language = "javascript";
                break;

            case "text/python":
                language = "python";
                break;
                
            case "text/markdown":
                language = "markdown";
                break;

            case "text/html":
                language = "html";
                break;

            case "text/css":
                language = "css";
                break;

            case "text/x-scss":
                language = "scss";
                break;
                
            case "application/x-lua":
                language = "lua";
                break;                

            case "text/ruby":
                language = "ruby";
                break;

            case "text/x-typescript":
                language = "typescript";
                break;

            case "wpm/descriptor":
            case "application/json":
            case "text/whenv2":
                language = "json";
                break;
        }

        if(MonacoEditor.extraTypes.has(self.fragment.type)) {
            console.log("Using extra type:", self.fragment.type);
            language = MonacoEditor.extraTypes.get(self.fragment.type);
        }

        requirejs(["vs/editor/editor.main"], (monaco) => {
            EventSystem.triggerEvent("Codestrates.Editor.Monaco.Loaded", {
                monaco: monaco
            });

            let theme = "vs";
            if (self.options.theme) theme = self.options.theme;
            if (self.options.theme === "dark") theme = "vs-dark";

            monaco.languages.json.jsonDefaults.diagnosticsOptions.enableSchemaRequest = true;

            self.editor = monaco.editor.create(self.editorDiv[0], {
                value: this.fragment.raw,
                language: language,
                theme: theme,
                minimap: {
                    enabled: false
                },
                automaticLayout: false,
                fixedOverflowWidgets: true,
                contextmenu: false,
                scrollBeyondLastLine: false,
                scrollBeyondLastColumn: 0,
                occurrencesHighlight: false,
                selectionHighlight: false,
                accessibilitySupport: "off",
                folding: false,
                guides: {
                    bracketPairs: true
                },
                readOnly: this.options.readOnly,
                bracketPairColorization: {
                    enabled: true
                },
                scrollbar: {
                    alwaysConsumeMouseWheel: false,
                    horizontal: 'auto',
                    vertical: 'auto',
                    horizontalScrollbarSize: 17
                }
            });

            self.editor.getModel().setEOL(0);

            if (self.options.mode === "full") {
                //Setup resizeing for all the lines

                self.editor.getModel().onDidChangeContent(()=>{self.updateSize();});
                self.editor.getModel().onDidChangeDecorations(()=>{
                    setTimeout(()=>{
                        self.updateSize();
                    }, 0);
                });

                self.updateSize();
            }

            self.editor.getModel().onDidChangeContent((evt) => {
                self.handleModelChanged();
            });

            self.editor.onDidChangeCursorSelection((evt)=>{
                self.triggerCursorSelection({
                    startLine: evt.selection.startLineNumber,
                    startColumn: evt.selection.startColumn,
                    endLine: evt.selection.endLineNumber,
                    endColumn: evt.selection.endColumn,
                    positionLine: evt.selection.positionLineNumber,
                    positionColumn: evt.selection.positionColumn
                });
            });

            self.triggerEditorOpened();
        });
    }

    focus() {
        let self = this;

        if(this.editor == null) {
            //Try to focus the editor as soon as its created.
            setTimeout(()=>{
                self.focus();
            }, 100);
        } else {
            this.editor.focus();
        }
    }

    setLine(line, column=1) {
        let self = this;

        if(this.editor == null) {
            //Try to focus the editor as soon as its created.
            setTimeout(()=>{
                self.setLine(line);
            }, 100);
        } else {
            this.editor.revealLineNearTop(line);
            this.editor.setPosition({
                column: column,
                lineNumber: line
            });
        }
    }   

    updateSize() {
        let oldWidth = this.editor.getLayoutInfo().width;
        let numLines = this.editor._modelData.viewModel._lines.getViewLineCount();

        let height = 19;

        let viewLines = this.editorDiv.find("div.view-line");

        if (viewLines.length > 0) {
            height = viewLines[0].offsetHeight;
        }

        this.editor.layout({
            width: oldWidth,
            height: numLines * height
        });

        let scrollHeight = this.editor.getScrollHeight();

        if(this.editor.getLayoutInfo().height !== scrollHeight) {
            this.editor.layout({
                width: oldWidth,
                height: scrollHeight
            });
        }
    }

    onSizeChanged() {
        if (typeof this.editor!=="undefined"){

            this.editor.layout();

            if(this.mode === "full") {
                this.updateSize();
            }
        }
    }
    
    setTheme(themeName){
        // STUB: Monaco does not support individual themes yet
        // See this on why this will change all editors simultaneously: https://github.com/Microsoft/monaco-editor/issues/338
        switch (themeName){
            case "dark":
                // this.editor._themeService
                monaco.editor.setTheme("vs-dark");
                break
            default:
                monaco.editor.setTheme("vs");
        }
    }

    updateForeignSelections(remoteClient = null) {
        let self = this;

        if(this.editor == null) {
            //Editor not ready yet.
            return;
        }

        let clientsToUpdate = [];

        if(remoteClient != null) {
            clientsToUpdate.push(remoteClient);
        } else {
            clientsToUpdate = Array.from(this.foreignSelections.keys());
        }

        clientsToUpdate.forEach((client)=>{
            let cursorSelection = self.foreignSelections.get(client);
            let decorations = self.foreignDecorators.get(client);

            if(decorations == null) {
                decorations = [];
            }

            let updatedDecoration = [];

            if(cursorSelection != null) {
                updatedDecoration.push({
                    range: new monaco.Range(cursorSelection.positionLine, cursorSelection.positionColumn, cursorSelection.positionLine, cursorSelection.positionColumn),
                    options: {
                        className: "otherCursor_" + client,
                        zIndex: 1
                    }
                });

                if (cursorSelection.startLine != cursorSelection.endLine || cursorSelection.startColumn != cursorSelection.endColumn) {
                    updatedDecoration.push({
                        range: new monaco.Range(cursorSelection.startLine, cursorSelection.startColumn, cursorSelection.endLine, cursorSelection.endColumn),
                        options: {
                            className: "otherSelection_" + client,
                            zIndex: 0
                        }
                    });
                }
            }

            decorations = self.editor.deltaDecorations(decorations, updatedDecoration);

            self.foreignDecorators.set(client, decorations);
        });
    }

    getValue() {
        if(this.editor == null) {
            return null;
        }
        
        return this.editor.getModel().getValue();
    }

    setValue(value) {
        if(this.editor == null) {
            return;
        }
        
        this.editor.getModel().setValue(value);
    }

    insertText(pos, val) {
        if(this.editor == null) {
            return;
        }
        
        let startPosition = this.editor.getModel().getPositionAt(pos);
        let range = monaco.Range.fromPositions(startPosition, startPosition);
        
        this.editor.getModel().applyEdits([{
            forceMoveMarkers: true,
            range: range,
            text: val
        }]);

        this.updateForeignSelections();
    }
    
    deleteText(pos, val) {
        if(this.editor == null) {
            return;
        }
        
        let startPosition = this.editor.getModel().getPositionAt(pos);
        let endPosition = this.editor.getModel().getPositionAt(pos+val.length);
        let range = monaco.Range.fromPositions(startPosition, endPosition);
        
        this.editor.getModel().applyEdits([{
            range: range,
            text: ""
        }]);

        this.updateForeignSelections();
    }

    insertAtSelection(text) {
        this.editor.executeEdits("draggedIntoEditor", [{
            identifier: {
                major: 1,
                minor: 1
            },
            range: this.editor.getSelection(),
            text: text,
            forceMoveMarkers: true
        }]);
    }

    unload() {
        //Kill monaco?
        if(this.editor != null) {
            this.editor.getModel().dispose();
            this.editor.dispose();
            this.editor = null;
        }

        super.unload();
    }

    setWordwrap(state) {
        if(this.editor != null) {
            this.editor.updateOptions({"wordWrap": state?"on":"off"});
        }
    }

    static types() {
        return [
            "text/javascript",
            "text/javascript+babel",
            "text/p5js",
            "text/whenjs",
            "text/whenv2",
            "text/varv",
            "text/varvscript",
            "text/python",
            "text/markdown",
            "text/html",
            "text/css",
            "text/ruby",
            "text/x-scss",
            "text/x-typescript",
            "application/x-lua",
            "wpm/descriptor",
            "model/vnd.usda",
            "application/json",
            "text/x-latex",
            ...MonacoEditor.extraTypes.keys()
        ];
    }
}; window.MonacoEditor = MonacoEditor;
MonacoEditor.extraTypes = new Map();

EditorManager.registerEditor(MonacoEditor);