monaco-languageclient icon indicating copy to clipboard operation
monaco-languageclient copied to clipboard

React Compatibility

Open wudstrand opened this issue 3 years ago • 36 comments

I am able to get the client example to connect with my python LSP server, but when I try to use the library in my react app it isn't working. I was wondering if we could get some docs on how to integrate with react, or if anyone knew of any guides on how to connect a custom LSP server to the react-monaco-editor.

wudstrand avatar Aug 23 '22 23:08 wudstrand

Hi @wudstrand

I try to use the library in my react app it isn't working

Can you be more specific about that? What does not work? We don't have a react specific example, only the webpack related one to show how to use it.

kaisalmen avatar Aug 24 '22 08:08 kaisalmen

Are you using @monaco-editor/react or react-monaco-editor? @monaco-editor/react won't work as it doesn't use the ESM version

What is the result of a npm list monaco-editor?

CGNonofr avatar Aug 24 '22 08:08 CGNonofr

I am using the @monaco-editor/react at the moment so I guess I need to transition over to react-monaco-editor library. Do I need to use web pack with this component? Here is a copy of my current impl of the editor in react. If you could let me know if I am on the right track that would be great.

import React, {useState} from 'react';
import Editor, {EditorProps, Monaco} from "@monaco-editor/react";
import {toSocket, WebSocketMessageReader, WebSocketMessageWriter} from "vscode-ws-jsonrpc";
import {
    CloseAction,
    ErrorAction,
    MessageTransports,
    MonacoLanguageClient,
} from "monaco-languageclient";
import normalizeUrl from 'normalize-url';



const ReconnectingWebSocket = require('reconnecting-websocket');


interface Props extends EditorProps {
    enableEdit?: boolean
}

const LSPConnectedEditor: React.FC<Props> = ({enableEdit, ...other}) => {
    const [editor, setEditor] = useState<any | null>(null)

    function createLanguageClient (transports: MessageTransports): MonacoLanguageClient {
        return new MonacoLanguageClient({
            name: 'Sample Language Client',
            clientOptions: {
                // use a language id as a document selector
                documentSelector: ['python'],
                // disable the default error handler
                errorHandler: {
                    error: () => ({ action: ErrorAction.Continue }),
                    closed: () => ({ action: CloseAction.DoNotRestart })
                }
            },
            // create a language client connection from the JSON RPC connection on demand
            connectionProvider: {
                get: () => {
                    return Promise.resolve(transports);
                }
            }
        });
    }

    function createWebsocket(socketUrl: string) {
        const socketOptions = {
            maxReconnectionDelay: 10000,
            minReconnectionDelay: 1000,
            reconnectionDelayGrowFactor: 1.3,
            connectionTimeout: 10000,
            maxRetries: Infinity,
            debug: false
        };
        return new ReconnectingWebSocket.default(socketUrl, [], socketOptions);
    }

    function createUrl (hostname: string, port: number, path: string): string {
        // eslint-disable-next-line no-restricted-globals
        const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
        return normalizeUrl(`${protocol}://${hostname}:${port}${path}`);
    }

    const beforeMount = (monaco: Monaco) => {
        monaco.languages.register({
            id: 'python',
            extensions: ['.py'],
            aliases: ['PYTHON', 'python', 'py'],
        });
    }

    const onMount = () => {

        // create the web socket
        const url = createUrl('localhost', 5000, '/');
        const webSocket = createWebsocket(url);

        webSocket.onopen = () => {
            const socket = toSocket(webSocket);
            const reader = new WebSocketMessageReader(socket);
            const writer = new WebSocketMessageWriter(socket);
            const languageClient = createLanguageClient({
                reader,
                writer
            });
            languageClient.start();
            reader.onClose(() => languageClient.stop());
        };
    }

    return (
        <Editor
            height="90vh"
            defaultLanguage={"python"}
            language={'python'}
            theme={'custom-theme'}
            beforeMount={(monaco: Monaco) => beforeMount(monaco)}
            onMount={(editor, monaco: Monaco) => onMount()}
            {...other}
        />
    )
}

export default LSPConnectedEditor;

Here is the output of npm list monaco-editor

+-- @monaco-editor/[email protected]
| +-- @monaco-editor/[email protected]
| | `-- [email protected] deduped
| `-- [email protected]
+-- [email protected]
| `-- [email protected] deduped
+-- [email protected]
| `-- [email protected] deduped
`-- [email protected]
  `-- vscode@npm:@codingame/[email protected]       
    `-- [email protected] deduped

wudstrand avatar Aug 24 '22 15:08 wudstrand

Webpack is not required, you can use whatever you want as soon as everything use the ESM version of monaco-editor from npm.

CGNonofr avatar Aug 24 '22 15:08 CGNonofr

Here is what I have so far. I am seeing an Uncaught Error: Missing service editorService error now.

Capture

import React, {useState} from 'react';
import {toSocket, WebSocketMessageReader, WebSocketMessageWriter} from "vscode-ws-jsonrpc";
import {
    CloseAction,
    ErrorAction,
    MessageTransports,
    MonacoLanguageClient,
} from "monaco-languageclient";
import normalizeUrl from 'normalize-url';
import MonacoEditor from "react-monaco-editor";
import {MonacoEditorProps} from "react-monaco-editor/src/types";
import * as monaco from "monaco-editor";


type Monaco = typeof monaco;
const ReconnectingWebSocket = require('reconnecting-websocket');


interface Props extends MonacoEditorProps {
    enableEdit?: boolean
}

const LSPConnectedEditor: React.FC<Props> = ({enableEdit, ...other}) => {
    const [editor, setEditor] = useState<any | null>(null)

    function createLanguageClient (transports: MessageTransports): MonacoLanguageClient {
        return new MonacoLanguageClient({
            name: 'Sample Language Client',
            clientOptions: {
                // use a language id as a document selector
                documentSelector: ['python'],
                // disable the default error handler
                errorHandler: {
                    error: () => ({ action: ErrorAction.Continue }),
                    closed: () => ({ action: CloseAction.DoNotRestart })
                }
            },
            // create a language client connection from the JSON RPC connection on demand
            connectionProvider: {
                get: () => {
                    return Promise.resolve(transports);
                }
            }
        });
    }

    function createWebsocket(socketUrl: string) {
        const socketOptions = {
            maxReconnectionDelay: 10000,
            minReconnectionDelay: 1000,
            reconnectionDelayGrowFactor: 1.3,
            connectionTimeout: 10000,
            maxRetries: Infinity,
            debug: false
        };
        return new ReconnectingWebSocket.default(socketUrl, [], socketOptions);
    }

    function createUrl (hostname: string, port: number, path: string): string {
        // eslint-disable-next-line no-restricted-globals
        const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
        return normalizeUrl(`${protocol}://${hostname}:${port}${path}`);
    }

    const beforeMount = (monaco: Monaco) => {
        monaco.languages.register({
            id: 'python',
            extensions: ['.py'],
            aliases: ['PYTHON', 'python', 'py'],
        });
    }

    const onMount = () => {
        console.log('Mounted')

        // create the web socket
        const url = createUrl('localhost', 5000, '/');
        const webSocket = createWebsocket(url);

        webSocket.onopen = () => {
            console.log("Opening Web socket connection")
            const socket = toSocket(webSocket);
            const reader = new WebSocketMessageReader(socket);
            const writer = new WebSocketMessageWriter(socket);
            const languageClient = createLanguageClient({
                reader,
                writer
            });

            languageClient.start()
                .then(() => console.log('language client started'))
                .finally(() => console.log('language client done'));

            reader.onClose(() => languageClient.stop());
        };
    }

    return (
        <MonacoEditor
            height="90vh"
            language={'python'}
            theme={'custom-theme'}
            editorWillMount={(monaco: Monaco) => beforeMount(monaco)}
            editorDidMount={(editor, monaco: Monaco) => onMount()}
            {...other}
        />
    )
}

export default LSPConnectedEditor;

wudstrand avatar Aug 24 '22 15:08 wudstrand

The issue is that react-monaco-editor has monaco-editor v0.33 as dependency while monaco-languageclient v3 REQUIRES monaco-editor 0.34, try to force the update to monaco-editor 0.34

CGNonofr avatar Aug 24 '22 16:08 CGNonofr

@CGNonofr we should add a table to the main README stating the version requirements. I was thinking about that before and will add something soon.

I was also thinking whether makes sense to have usage examples for react, angular, vue and webpack outside this repo. First, we can move unneeded things out of here (removing unneeded dependencies from the examples) and to be able to point people to existing resources. I will see if I do this in the upcoming weeks whenever there is time (lower prio).

kaisalmen avatar Aug 25 '22 07:08 kaisalmen

we should add a table to the main README stating the version requirements

It can't hurt :+1:

I was also thinking whether makes sense to have usage examples for react, angular, vue and webpack outside this repo

Why does it need to be outside of the repo?

CGNonofr avatar Aug 25 '22 08:08 CGNonofr

Why does it need to be outside of the repo?

Yeah, god question. Maybe it is better to create two workspaces: one for the two libs and another on for the examples. Lean dependencies for the libs and whatever is needed can go in the examples.

kaisalmen avatar Aug 25 '22 09:08 kaisalmen

Yeah, god question

Cima_da_Conegliano,_God_the_Father

CGNonofr avatar Aug 25 '22 09:08 CGNonofr

🤣 Good, of course

kaisalmen avatar Aug 25 '22 09:08 kaisalmen

I'm not sure why the angular/react demos can't be along with the other demos though

CGNonofr avatar Aug 25 '22 09:08 CGNonofr

I'm not sure why the angular/react demos can't be along with the other demos though

Yes, they can. I would just like to have separated npm workspaces, one for monaco-languageclient and vscode-ws-jsonrpc and one for all current and future examples.

kaisalmen avatar Aug 25 '22 09:08 kaisalmen

I know that I threw it all together a couple of months ago, but I now think it is better to have a separation here.

kaisalmen avatar Aug 25 '22 09:08 kaisalmen

My issue may be different, but I also started receiving the following error:

image

+-- [email protected]
`-- [email protected]
  `-- vscode@npm:@codingame/[email protected]
    `-- [email protected] deduped

I am using the monaco-editor/language-client in a vue application.

I added the window.MonacoEnvironment code below recently, so I'm trying to determine if this is the culprit. Perhaps I'm doing something wrong here, but I was just trying to solve this warning:

image

import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";

export function useMonacoEnvironment() {
  // @ts-ignore
  if (!window.MonacoEnvironment) {
    console.debug("Setting up monaco environment");
    // @ts-ignore
    window.MonacoEnvironment = {
      getWorker(_: any, label: string) {
        if (label === "json") {
          return new jsonWorker();
        }
        if (label === "css" || label === "scss" || label === "less") {
          return new cssWorker();
        }
        if (label === "html" || label === "handlebars" || label === "razor") {
          return new htmlWorker();
        }
        if (label === "typescript" || label === "javascript") {
          return new tsWorker();
        }
        return new editorWorker();
      },
    };
  }
}

colinblaise avatar Aug 25 '22 13:08 colinblaise

The "missing service" error happens when there is multiple monaco editor versions involved

CGNonofr avatar Aug 25 '22 13:08 CGNonofr

I am only seeing v 0.33.0 in my yarn.lock file. But thanks I'll keep digging around with that in mind

colinblaise avatar Aug 25 '22 13:08 colinblaise

@CGNonofr FYI I have just created a couple of enhancement issues for creating framework related examples.

kaisalmen avatar Aug 25 '22 13:08 kaisalmen

I am only seeing v 0.33.0 in my yarn.lock file. But thanks I'll keep digging around with that in mind

That's weird, because monaco-languageclient@3 has a dependency on [email protected] which has a dependency on [email protected]

what is the result of npm list monaco-editor?

CGNonofr avatar Aug 25 '22 14:08 CGNonofr

I put the output of that command in my first response. I'm not using monaco-languageclient@3, should I be?

+-- [email protected]
`-- [email protected]
  `-- vscode@npm:@codingame/[email protected]
    `-- [email protected] deduped

colinblaise avatar Aug 25 '22 14:08 colinblaise

Oh you're not using the last version of monaco-languageclient! I don't know about your error, you should probably update though

CGNonofr avatar Aug 25 '22 14:08 CGNonofr

Updated to v3 and still receiving the same error. I'll figure this out.

+-- [email protected]
`-- [email protected]
  `-- vscode@npm:@codingame/[email protected]
    `-- [email protected] deduped

colinblaise avatar Aug 25 '22 14:08 colinblaise

I just realize it can happen if an editor is created before monaco-languageclient/monaco-vscode-api is loaded (because creating an editor initialize and freeze the services, while we are trying to register additional ones)

Otherwise I don't know, I'm interested it you find anything

CGNonofr avatar Aug 25 '22 14:08 CGNonofr

Updated to v3 and still receiving the same error. I'll figure this out.

Plus, you don't need to define monaco-editor separately in your dependencies in the package.json? If so, remove it and only keep the dependency to monaco-languageclient.

kaisalmen avatar Aug 25 '22 14:08 kaisalmen

Since monaco-editor is imported directly, I think it's a good idea to have it in the package.json

CGNonofr avatar Aug 25 '22 14:08 CGNonofr

Yes, but you have to keep version in sync 🙂

kaisalmen avatar Aug 25 '22 14:08 kaisalmen

I figured out how to reproduce my issue.

I am instantiating multiple monaco editors on a page without setting up any language services. These are just simple read-only views.

image

If I then navigate to a page that instantiates an editor with language services, it throws the error.

It appears that there may be some initialization logic that only occurs once. So if I load an editor without language services, and then load it with language services, the second load fails.

edit: When I say language services, I am referring to calling MonacoServices.install();

colinblaise avatar Aug 25 '22 14:08 colinblaise

Yep, exactly what I meant, you can do import vscode before starting your first editor to prevent the error

CGNonofr avatar Aug 25 '22 14:08 CGNonofr

Oh, I didn't see your comment earlier about loading monaco-languageclient/monaco-vscode-api!

I'm not 100% sure what you mean by import vscode.

Simply add an import statement? import vscode from "vscode"? Or is vscode coming from somewhere in monaco-editor or monaco-languageclient

colinblaise avatar Aug 25 '22 14:08 colinblaise

Simply importing vscode should do the trick, importing monaco-languageclient should work as well

We use an alias from vscode to @CodinGame/monaco-vscode-api

CGNonofr avatar Aug 25 '22 14:08 CGNonofr