SoftTimur
SoftTimur

Reputation: 5520

Hover providers in case of multiple monaco editors

I have built a component ResizableMonacoEditor on top of Monaco Editor. In index.tsx, we have

import * as monaco from 'monaco-editor';
const MonacoEditor = lazy(() => import('react-monaco-editor'));

monaco.languages.register({ id: "mySpecialLanguage", extensions: [], aliases: [], mimetypes: [], });

class ResizableMonacoEditor extends React.Component {

  _handleEditorDidMount(editor, monaco) {
      ... ...
      monaco.languages.registerHoverProvider("mySpecialLanguage", {
        provideHover: (model, position, token) => {
            return {
                range: new monaco.Range(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount())),
                contents: [
                    { value: '**SOURCE**' }
                ]
            };
        }
    });
  }
  ... ...
  render() {
    return <MonacoEditor {...this.props} editorDidMount={this._handleEditorDidMount.bind(this)} />
  }
}

It works when we have only 1 ResizableMonacoEditor on a page. Now, I would like to have two ResizableMonacoEditor on a page. They are all supposed to hold programs in mySpecialLangauge.

Then, I realize that each editor registers one hoverProvider. As a consequence, when I hover on a text in one editor, two **SOURCE** are shown in the message. But, I think only the hoverProvider registered by the editor should be executed.

Does anyone know what is the correct way to organize language, editor, model, and registerHoverProvider in case of multiple monaco editors?

Upvotes: 3

Views: 312

Answers (1)

Zachary Duvall
Zachary Duvall

Reputation: 436

It's been a couple years, but I recently encountered this same issue and found a solution that reliably prevents hover duplication when you have 2+ Monaco editors.

Solution

To solve it, I introduced a mechanism to track which Monaco model is currently being hovered and only show hover content for that model. Here's how you can implement it:

  1. Use a useRef to store the id of the Monaco model being hovered:
import { useRef } from 'react';


const hoveredMonacoModelId = useRef<string | null>(null);
  1. Configure onMouseMove and onMouseLeave event callbacks for each editor to set the ref appropriately.
    Note: Some kind of onMouseEnter event callback would be preferable, but unfortunately Monaco doesn't expose one. The onMouseMove event seems to be the best alternative. This fires much more frequently than needed, but because we're just setting a ref, the impact is negligible.
import { Editor } from '@monaco-editor/react';

<Editor
  {...otherProps}
  onMount={(editor) => {
    const monacoModelId = editor.getModel()?.id ?? null;
    editor.onMouseMove(() => (hoveredMonacoModelId.current = monacoModelId));
    editor.onMouseLeave(() => (hoveredMonacoModelId.current = null));
  }}
/>;

  1. Inside a useEffect where the hover provider is registered, compare the hoveredMonacoModelId's current value with the id of the hovered model.
import { useMonaco } from '@monaco-editor/react';

const monaco = useMonaco();

useEffect(() => {
  if (!monaco) return;

  const hoverProvider = monaco.languages.registerHoverProvider('json', {
    provideHover: (hoverModel, position) => {
      // return early when not hovering the expected model
      if (hoverModel.id !== hoveredMonacoModelId.current) return null;

      return {
        contents: [{ value: '**Example Hover Content**' }],
      };
    },
  });

  return () => hoverProvider.dispose();
}, [monaco]);

Doing this makes sure the hover provider only activates for the relevant editor's model.

Upvotes: 2

Related Questions