Search code examples
reactjsmonaco-editorreact-monaco-editor

Set up listeners when a model is created or disposed for @monaco-editor/react


I have a web application containing a Monaco Editor. Previously, I used react-monaco-editor, now for some reasons, I'm thinking of switching to @monaco-editor/react.

With react-monaco-editor, I was able to code a DiagnosticsAdapter class to provide real-time error checking and suggestions in a code editor. When a user types code into the editor, the adapter will validate the code and provide feedback on any issues it finds.

Now, with @monaco-editor/react, the DiagnosticsAdapter does not work anymore: code in onModelAdd and code in onModelRemoved is never executed.

enter image description here

Does anyone know how to set up these listeners for @monaco-editor/react and make DiagnosticsAdapter work?

Here is CodeSandBox: https://codesandbox.io/s/aged-voice-5rz8z2?file=/src/App.js:0-542

Here is language-feature.ts:

import * as monaco from "monaco-editor";

export class DiagnosticsAdapter {
  private _disposables: monaco.IDisposable[] = [];
  private _listener = Object.create(null);

  constructor() {
    console.log("herehere in constructor");

    const onModelAdd = (model: monaco.editor.IModel): void => {
      console.log("herehere onModelAdd");

      let handle;
      const changeSubscription = model.onDidChangeContent(() => {
        clearTimeout(handle);
        handle = setTimeout(() => console.log("call validation"), 500);
      });

      this._listener[model.uri.toString()] = {
        dispose() {
          changeSubscription.dispose();
          clearTimeout(handle);
        }
      };

      console.log("call vailidation");
    };

    const onModelRemoved = (model: monaco.editor.IModel): void => {
      console.log("herehere onModelRemoved");
      monaco.editor.setModelMarkers(model, "abc", []);
      const key = model.uri.toString();
      if (this._listener[key]) {
        this._listener[key].dispose();
        delete this._listener[key];
      }
    };

    console.log("herehere 5");
    this._disposables.push(monaco.editor.onDidCreateModel(onModelAdd));
    console.log("herehere 6");
    this._disposables.push(monaco.editor.onWillDisposeModel(onModelRemoved));
    console.log("herehere 7");
    this._disposables.push(
      monaco.editor.onDidChangeModelLanguage((event) => {
        console.log("herehere 8");
        onModelRemoved(event.model);
        console.log("herehere 9");
        onModelAdd(event.model);
      })
    );

    console.log("herehere 10");
    this._disposables.push({
      dispose() {
        for (const model of monaco.editor.getModels()) {
          onModelRemoved(model);
        }
      }
    });

    console.log("herehere 11");
    monaco.editor.getModels().forEach(onModelAdd);
  }

  public dispose(): void {
    this._disposables.forEach((d) => d && d.dispose());
    this._disposables = [];
  }
}

Here is App.js:

import React from "react";
import Editor from "@monaco-editor/react";
import { DiagnosticsAdapter } from "./language-feature";

function App() {
  function handleEditorDidMount(editor, monaco) {
    monaco.languages.register({ id: "mySpecialLanguage" });

    let x = new DiagnosticsAdapter();
  }

  return (
    <div className="App">
      <Editor
        height="90vh"
        defaultLanguage="mySpecialLanguage"
        defaultValue="// some comment"
        onMount={handleEditorDidMount}
      />
    </div>
  );
}

export default App;

Solution

  • To manipulate models with @monaco-editor/react, you should use the handleEditorDidMount method to access the editor instance and the editor object itself, which has the methods you need for handling models.

    Here's an example:

    function App() {
      function handleEditorDidMount(editor, monaco) {
        monaco.languages.register({ id: "mySpecialLanguage" });
    
        // Initialize the DiagnosticAdapter here.
        let diagnosticAdapter = new DiagnosticsAdapter(editor, monaco);
      }
    
      return (
        <div className="App">
          <Editor
            height="90vh"
            defaultLanguage="mySpecialLanguage"
            defaultValue="// some comment"
            onMount={handleEditorDidMount}
          />
        </div>
      );
    }
    
    export default App;
    

    Here's an updated DiagnosticsAdapter class:

    import * as monaco from "monaco-editor";
    
    export class DiagnosticsAdapter {
      private _disposables: monaco.IDisposable[] = [];
      private _listener = Object.create(null);
      private _editor: monaco.editor.IStandaloneCodeEditor;
      private _monaco: typeof monaco;
    
      constructor(editor: monaco.editor.IStandaloneCodeEditor, monaco: typeof monaco) {
        this._editor = editor;
        this._monaco = monaco;
    
        console.log("herehere in constructor");
    
        const onModelAdd = (model: monaco.editor.IModel): void => {
          console.log("herehere onModelAdd");
    
          let handle;
          const changeSubscription = model.onDidChangeContent(() => {
            clearTimeout(handle);
            handle = setTimeout(() => console.log("call validation"), 500);
          });
    
          this._listener[model.uri.toString()] = {
            dispose() {
              changeSubscription.dispose();
              clearTimeout(handle);
            }
          };
    
          console.log("call vailidation");
        };
    
        const onModelRemoved = (model: monaco.editor.IModel): void => {
          console.log("herehere onModelRemoved");
          this._monaco.editor.setModelMarkers(model, "abc", []);
          const key = model.uri.toString();
          if (this._listener[key]) {
            this._listener[key].dispose();
            delete this._listener[key];
          }
        };
    
        // Get the model for the current editor instance.
        let model = this._editor.getModel();
    
        if (model) {
          onModelAdd(model);
        }
    
        console.log("herehere 5");
        this._disposables.push(model.onWillDispose(() => onModelRemoved(model)));
        console.log("herehere 6");
        this._disposables.push(
          this._editor.onDidChangeModel((event) => {
            console.log("herehere 8");
            onModelRemoved(event.oldModel);
            console.log("herehere 9");
            onModelAdd(event.newModel);
          })
        );
    
        console.log("herehere 10");
        this._disposables.push({
          dispose() {
            onModelRemoved(model);
          }
        });
    
        console.log("herehere 11");
      }
    
      public dispose(): void {
        this._disposables.forEach((d) => d && d.dispose());
        this._disposables = [];
      }
    }
    

    Edit: Note that editor.onDidChangeModel only fires when the model associated with the editor changes, not when any model in the system changes.