Search code examples
vue.jsvuejs3monaco-editor

addCommand using wrong prop in Monaco + Vue3


Likely not specific to Monaco, but below is an example demonstrating the issue. I have a Vue 3 application that has state which is a list of objects. The application renders multiple components that contain an instance of a Monaco editor, and I'm passing in each state object to the components and using that state as a property within the component. I'm attaching a command to the editor in which case the property contains the incorrect state. It's using the last state object in the list. This is likely clearer using an example:

types.ts

export type MyObj = {
    code: string;
};

App.vue

<script setup lang="ts">
import { ref } from 'vue';
import { type MyObj } from './components/types';
import CodeEditor from './components/CodeEditor.vue';

const myObjs = ref<MyObj[]>([
  { code: 'foo' },
  { code: 'bar' },
  { code: 'biz' },
])
</script>

<template>
  <CodeEditor v-for="obj in myObjs" :obj="obj" :key="obj.code" />
</template>

CodeEditor.vue

<script setup lang="ts">
import { defineProps, onMounted, type PropType, ref } from 'vue';
import * as monaco from 'monaco-editor';
import type { MyObj } from './types';

const props = defineProps({
  obj: {
    type: Object as PropType<MyObj>,
    required: true,
  }
});

const editorContainer = ref<HTMLDivElement | null>(null);

onMounted(() => {
    console.log('onMounted, code = ', props.obj.code);

    if (!editorContainer.value) {
        return;
    }

    const editor = monaco.editor.create(editorContainer.value, {
        value: props.obj.code,
        language: 'python',
        automaticLayout: true,
    });

    editor.getModel()?.onDidChangeContent(() => {
        console.log('Content changed, code = ', props.obj.code);
    });

    editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
        console.log('Cmd + Enter, code = ', props.obj.code);
    });
});
</script>

<template>
    <div ref="editorContainer" class="editor"></div>
</template>

<style>
.editor {
    min-height: 100px;
    border: 2px solid #ccc;
    margin-bottom: 10px;
}
</style>

Notice in this CodeEditor component, I'm attaching the following key command (Cmd + Enter):

editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
    console.log('Cmd + Enter, code = ', props.obj.code);
})

This is where the problem lies.

When rendering the application, as expected, the messages get logged from onMounted as:

onMounted, code = foo
onMounted, code = bar
onMounted, code = biz

However, when I execute the key command from each of the three editor, in order, I get:

Cmd + Enter, code =  biz
Cmd + Enter, code =  biz
Cmd + Enter, code =  biz

Which, biz is the last item in the list. Strangely enough, notice that I also attached a onDidChangeContent event to the editor too:

editor.getModel()?.onDidChangeContent(() => {
    console.log('Content changed, code = ', props.obj.code);
});

When entering a new character in the three editors, in order, I see the correct code from props:

Content changed, code =  foo
Content changed, code =  bar
Content changed, code =  biz

Any help or nudge in the right direction would be appreciated. Thanks.


Solution

  • For those that stumble across this question, the issue appears to be related to Monaco and not Vue. There is an open bug in Monaco when using addCommand when there are multiple instances of an editor on the same page. The suggestion in the bug report above is to use actions, if possible, as a work around. In the case of the code in the original question, using actions, it would become:

    editor.addAction({
        id: 'execute',
        label: 'execute',
        keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
        run() {
            console.log('Cmd + Enter, code = ', props.obj.code)
        }
    });
    

    This appears to resolve the issue.