Oct 30th 2025
Mark Sujew
Notebook internals in Theia
Mark explains how native Jupyter-style notebooks were implemented in Theia, detailing key architecture choices and performance improvements.
Through the past years we have been integrating all kinds of different JavaScipt code editors, like Ace, Orion or CodeMirror into custom web-based software tools. Since last June another very good editor, has been available: Microsoft’s Monaco editor, the editor widget that is used at the core of VSCode. Besides the very good quality and speed of the editor, the API is very close to the Language Server Protocol (LSP), which is not a surprise given that this is all developed by the same team. At TypeFox, we love both the Monaco Editor and the Language Server Protocol and use them extensively in our projects. One publicly available project is RIDE, a Data-Science IDE we developed for R-Brain. Another publicly available example is the web calc example, which is covered by Akos here. In such cases, we connect Monaco editors with language servers running remotely. So far, however, Monaco did not speak the LSP out of the box, so we had to do a lot of plumbing and shimming to make it work. This has now been generalized and published as individual npm packages under the MIT license. The Monaco Editor Language Client provides a language client that establishes communication between Monaco editors and language servers over JSON-RPC and the VSCode WebSocket JSON-RPC package enables JSON-RPC to work over web sockets. The language client package comes with an example showing how Monaco editor can be connected with JSON language server. Go ahead, check out this repository, follow an instruction to start and play with the example, and then come back for a detailed explanation.
The server side consists of two components: Express Server and JSON Language Server. We use a web application framework for node.js called Express to serve static content, as index.html and js-code, and open a web socket connection. Instead of implementing our own JSON language server we deploy VSCode JSON language service package as a language server by means of VSCode Language Server package (consult VSCode documentation to learn more about it).
The JSON language server can be deployed as an external process, or within the express server’s process:
What if you want to connect your Xtext language server instead of an example one? You have two options:
An entry client page provides a container for the Monaco editor and loads a client side code bundled by webpack. Unfortunately, Monaco is only distributed as an AMD module and cannot be bundled by webpack. To overcome this difficulty one should ensure that Monaco code is loaded before client code.
Once Monaco code is loaded but before starting Monaco Language Client one should:
monaco.languages.register({
id: 'json',
extensions: ['.json', '.bowerrc', '.jshintrc', '.jscsrc', '.eslintrc', '.babelrc'],
aliases: ['JSON', 'json'],
mimetypes: ['application/json'],
});
Default services notify the language client about changes in the Monaco editor models, hook up the Monaco language features with the language client, e.g. a completion, hover, etc., and provide means to log messages from language servers in the console. const services = createMonacoServices();
// create the web socket
const url = createUrl('/sampleServer');
const webSocket = createWebSocket(url);
// listen when the web socket is opened
listen({
webSocket,
onConnection: connection => {
// create and start the language client
const languageClient = createLanguageClient(connection);
const disposable = languageClient.start();
connection.onClose(() => disposable.dispose());
}
});
Having everything wired up, one can start the actual client. In this example, we use a reconnecting web socket package to auto reopen the web socket connection if one is closed. Because of it, we have to disable the default error handling, which tries to restart the language client 5 times and start a new language client each time when a new web socket connection is opened.
function createLanguageClient(connection: MessageConnection): BaseLanguageClient {
return new BaseLanguageClient({
name: "Sample Language Client",
clientOptions: {
// use a language id as a document selector
documentSelector: ['json'],
// disable the default error handler
errorHandler: {
error: () => ErrorAction.Continue,
closed: () => CloseAction.DoNotRestart
}
},
services,
// create a language client connection from the JSON RPC connection on demand
connectionProvider: {
get: (errorHandler, closeHandler) => {
return Promise.resolve(createConnection(connection, errorHandler, closeHandler))
}
}
})
}