[Part 2] Connect Monaco React Web Editor with Language Server using WebSocket… How hard can it be?
In the previous article I have gone through how to set up the web editor and on a react app. Well there are few things have changed like the versions and what libraries to used. I updated the libraries to latest compatible versions in the previous repo. Now in this article I will talk about how we can connect a language server on to our web editor. The language server that I will use will be the WebSocket fronted implementation of this language server example implementation.
How?
Well there are few ways we can archive this.
- One is to build your language server as a javascript library and connect it to a compiler backend or have the compiler of your language be a javascript library as well.
- Second is to connect the editor to a language server running on a server using WebSocket or Http
Both methods are valid but heavily dependent on the use case. If you don’t want to have the language server as a javascript library or doesn’t have the compiler which is a javascript library first method won’t work. Similar situation can affect the second method as well.
In this article I will go with the second method and I will use WebSocket to connect to a language server which running remotely.
Architecture
Implementation
Here I will explain the additional parts that we need for the implementation that we discussed in the previous articles.
Backend Implementation
Full Backend implementation can be found at the below GitHub branch
Note: Here the LSP4J version is bumped to 0.21.0. If you want you can go higher. Previously we were using LSP4J 0.9.0.
In this WebSocket service implementation what I have used is SpringBoot starter kit for WebSockets. Language Server Implementation hasn’t change and only the Launcher of the Language Server has changed.
What I have used in this WebSocket Launcher implementation is that the WebSocket that handles Text messages.
This is the class that handles the JSON RPC messages send from the WebEditor WebSocket. It will receive the messages as a text and then using message handlers it will parse it to JSON RPC and pass on to LSP4J JSON RPC handler.
You can build this using bellow command in your terminal as we are using Maven for the builder.
mvn clean install
And to run the program, after building, you can use below command.
java -jar target/hellols-0.0.1-SNAPSHOT.jar
I won’t go in to more details as this implementation is about getting the SpringBoot WebSocket server running. And If there are SpringBoot Experts please improve this code and do contribute to the project.
Frontend Implementation
Full frontend implementation can be found at the below GitHub branch.
In the frontend implementation for the WebEditor what has changed is how we create the Monaco Editor model and we have register our `Hello` Language to Monaco Editor. Also we have added a WebSocket client to connect the Monaco Language Client to Remote LS.
Let’s first look at the package.json to understand what we have added additionally.
If you look at it I have changed the builder tool to Vite from Webpack. Reason behind this change is because there were few build issues when comes to `monaco-languageclient` when build using Webpack. Some dependencies were not getting resolved in webpack due to some reason hence I changed to Vite 😁 ( Most simple solution when in need, change all😛).
Next changes are I have added vscode-ws-jsonrpc and vscode-languageclient libraries. These will enable monaco-languageclient to send and receive messages through WebSocket connection. Let’s look at the implementation of the WebSocket Client to understand.
here if you look at the connectToLs() function you can see that we have Opened a WebSocket connection for the LS_WS_URL.
const webSocket = new WebSocket(LS_WS_URL);
then if the connection is open successfully then we are connecting the WebSocket messages to vscode-ws-jsonrpc library to define incoming ( read) and outgoing (write) serialization JSON RPC messages from LS to WebEditor and vise versa.
const socket = toSocket(webSocket);
const reader = new WebSocketMessageReader(socket);
const writer = new WebSocketMessageWriter(socket);
const languageClient = new MonacoLanguageClient({
name: `${HELLO_LANG_ID} Language Client`,
clientOptions: {
documentSelector: [HELLO_LANG_ID],
errorHandler: {
error: () => ({ action: ErrorAction.Continue }),
closed: () => ({ action: CloseAction.DoNotRestart })
}
},
connectionProvider: {
get: () => Promise.resolve({reader, writer}),
},
});
Then once we have define the LanguageClient with appropriate reader and writer we can start the Language Client as below.
languageClient.start();
Then the Most important question is where do we initialize the WebSocket connection?
So initialization of the WebSocket can be done inside the EditorDidMount() callback function.
If you look at the EditorDidMount callback implementation you can see we have done few steps.
registerLanguage(): This function will perform a crucial configuration for the Monaco editor which is registering our Custom Language.
export const registerLanguage = () => {
monaco.languages.register({
id: HELLO_LANG_ID,
aliases: [HELLO_LANG_ID],
extensions: [HELLO_LANG_EXTENSION]
});
}
This will tell Monaco editor to not to rely on defined languages and treat all source as a custom language. If we didn’t do this Monaco editor won’t send messages using Monaco Language Client.
createModel(): This will create a new Monaco Editor model with a file URI. If we didn’t create this model Monaco will use the default model which uses a in memory file URI which will cause issues when comes to LS.
export const createModel = (): monaco.editor.ITextModel => monaco.editor.createModel(
'',
HELLO_LANG_ID,
monaco.Uri.parse(
`file:///hello-${Math.random()}${HELLO_LANG_EXTENSION}`
)
);
this is just a mock file URI that has been added but in case you have actual file URI please add that path. Our WebEditor is not implemented to actually handle files on the file system.
editor.setModel(model);
Then we set the created editor model to mounted editor as above.
After preparing the Monaco Editor, the next step is to connect the Monaco Language Client to WebSocket. This is where we call the connectToLs().
connectToLs();
Well That’s it. Now it should be ready to go.
You can build the Frontend using below command
npm run build
And you can run the Frontend using below command
npm run dev
Important Facts
- Make sure you run the backend and then run the frontend as WebSocket client is initialize as soon as the Monaco Editor mount to the DOM.
- If you are bumping the Monaco library versions or any related library version make sure you bump surrounding libraries to compatible versions.
- Check the LSP4J version and Monaco-LanguageClient version implements the same LSP specification.