Search code examples
reactjsevent-handlingmonaco-editor

Is it ok to addCommand on Monaco Editor as my states change (react app)?


I'm using @monaco-editor/react. I need to implement a feature in which the code within the editor runs when the user presses SHIFT+ENTER.

The obvious choice to deal with this feature would be an onKeyUp event listener. But on monaco you cannot preventDefault the keyup event. The running part worked when I tried to use that approach but shift+enter would still create a non-desired new line (I could listen to keyUp on the whole document but that fell hackier than I'd like).

So I had to do it using commands. Again, the obvious choice would be adding the command on editor mount. But once the command is added none of the state var changes are reflected on the command's callback.

For instance, to avoid unnecessary code runs I have to check if the code within the editor has changed since its last run. The variable that stores the code running state is a boolean whose truthiness is checked before anything on the command's callback as a condition to run the code.

It appears that, somehow, monaco is managing to copy a snapshot of all the variables within the callback function and assigning it to the command rather than getting it by reference.

I could only get it working by reassigning all the commands whenever a state change occurs on a useEffect hook.

All that said, I have three questions:

  1. Is it ok to do it like that?
  2. Will it suffer from performance after reassigning the same command over and over again?
  3. Is there another way to do that?

Solution

  • The right way to accomplish that is not to get Monaco in the way, which does all the internal handling. Instead use addCommand or addAction. For example in my code editor implementation I want to prevent that a line break is deleted by pressing backspace, so I add a command:

    editor.addCommand(KeyCode.Backspace, this.handleBackspace);
    

    and then I can do whatever I like, including nothing, to ignore the keypress. You have to trigger edits in Monaco to apply the keypress, if you want something to happen:

       /**
         * Handles input of a single backspace keypress. This is used to block removing execution contexts when the caret
         * is at the start of the context and the user presses backspace.
         */
        private handleBackspace = (): void => {
            const editor = this.backend;
            const model = this.model;
            if (editor && model) {
                const selections = editor.getSelections();
                selections?.forEach((selection) => {
                    if (!selection.isEmpty()) {
                        // Simply remove the selection, if it is not empty.
                        const op = { range: selection, text: "", forceMoveMarkers: true };
                        editor.executeEdits("delete", [op]);
                    } else {
                        // No selection -> single character deletion.
                        const context = model.executionContexts.contextFromPosition({
                            lineNumber: selection.startLineNumber,
                            column: selection.startColumn,
                        });
    
                        if (context) { // Should always be assigned.
                            // Do nothing with this position if the user going to remove the line break that separates
                            // execution contexts.
                            if (context.startLine === selection.startLineNumber && selection.startColumn === 1) {
                                // Do nothing.
                            } else {
                                const range = {
                                    startLineNumber: 0,
                                    startColumn: 0,
                                    endLineNumber: 0,
                                    endColumn: 0,
                                };
    
                                if (selection.startColumn > 1) {
                                    range.startLineNumber = selection.startLineNumber;
                                    range.startColumn = selection.startColumn - 1;
                                    range.endLineNumber = selection.startLineNumber;
                                    range.endColumn = selection.startColumn;
                                } else if (selection.startLineNumber > 1) {
                                    const previousColumn = model.getLineMaxColumn(selection.startLineNumber - 1);
                                    range.startLineNumber = selection.startLineNumber;
                                    range.startColumn = selection.startColumn;
                                    range.endLineNumber = selection.startLineNumber - 1;
                                    range.endColumn = previousColumn;
                                }
    
                                const op = { range, text: "", forceMoveMarkers: true };
                                editor.executeEdits("backspace", [op]);
                            }
                        }
                    }
                });
            }
        };
    

    For more examples of key input handling check the prepareUse() method in the MySQL for VS Code extension code.