For a cloud development platform, a good Web IDE
code editor can greatly improve the user's coding experience, and an Web IDE
important part of a code editor is the code editor.
There are currently a variety of code editors on the web to choose from, such as Ace
, CodeMirror
, Monaco
and . The comparison of these three editors is introduced in detail in this article , so I won’t go into details here. In this article, we choose Monaco Editor
to integrate LSP so that it can theoretically support all programming languages.
Original link: https://forum.laf.run/d/1027
What is LSP
LSP (Language Server Protocol), also known as Language Service Protocol, more specifically and popularly speaking, defines a set of specifications between the code editor and the language server, thus allowing the original
m
n
Correspondence between editors and programming languages
become
m
The relationship between an editor and LSP
, and the relationship between n
a programming language and LSP
,
m*n
This reduces the development complexity m+n
from
In addition to being friendly to editor developers and programming language developers, it is also more friendly to developers like us who try to make an editor support multiple languages. With such an editor in front of us, we can easily adapt to the vscode
design vscode
of Ideas to realize our needs.
Preview
In this article, we will develop the smallest and most lightweight editor Demo
as a demonstration. The architecture is very simple, that is, creating a front-end and a language server at the back-end. The two are transmitted Monaco Editor
through vscode-ws-jsonrpc
and services. The actual implementation of the The editor is as follows:WebSocket
Web
Python
Server side development
Before Web
the client can access the language service, we must first run a language service on the server. https://langserver.org/ This website contains many language service implementations.
Here we choose the language service officially maintained by Microsoft pyright
to provide language services
First create Express
the server, configure the static file service, use fileURLToPath
and dirname
to get the path of the current file, and set the service on 30000
the port
const app = express();
const __filename = fileURLToPath(import.meta.url);
const dir = dirname(__filename);
app.use(express.static(dir));
const server = app.listen(30000);
Then we need to create a WebSocket Server. Pay attention to the noServer parameter here. If noServer is not specified, WebSocketServer will automatically create an http server to handle the upgrade of the browser's HTTP request to the WebSocket request.
const wss = new WebSocketServer({
noServer: true,
});
Here we need to create our own HTTP server and manually handle the browser's upgrade request. The following code is how to listen to the upgrade event and process it.
server.on('upgrade',()=>{});
In the processing function, follow the following code to use WebSocket into the jsonrpc protocol, and start the language server to connect the two.
First build the path to the language server and find the location of the copyright package.
const baseDir = resolve(getLocalDirectory(import.meta.url));
const relativeDir = '../../../node_modules/pyright/dist/pyright-langserver.js';
const ls = resolve(baseDir, relativeDir);
Create a connection to the language server and create a data connection to the WebSocket
const serverConnection = createServerProcess(serverName, 'node', [ls, '--stdio']);
const reader = new WebSocketMessageReader(socket);
const writer = new WebSocketMessageWriter(socket);
const socketConnection = createConnection(reader, writer, () => socket.dispose());
Finally, use the forward function to forward the message from socketConnection to serverConnection, as follows:
forward(socketConnection, serverConnection, message => {
if (Message.isRequest(message)) {
console.log(`Received:`);
console.log(message);
}
if (Message.isResponse(message)) {
console.log(`Sent:`);
console.log(message);
}
return message;
});
So we ran the language server and entered something casually into the front-end editor that will be written later in the article. You can see that the message is output in the terminal.
In this way, we have completed the development of the language server using copyright.
Web development
Next we develop the front-end content, still in the website above,
It can be seen that Monaco Editor also has a solution to support LSP, so we use the monaco-languageclient developed by TypeFox to develop integrated LSP with monaco.
First, use the initServices function of monaco-languageclient to initialize some services. The most important of them are the following four configurations, which define Monaco's language services and theme display.
await initServices({
enableModelService: true,
enableThemeService: true,
enableTextmateService: true,
enableLanguagesService: true,
})
Then create a WebSocket connection to the language server.
createWebSocket("ws://localhost:30000/pyright");
Creating this connection also requires the use of monaco-languageclient.
const createWebSocket = (url: string): WebSocket => {
const webSocket = new WebSocket(url);
webSocket.onopen = async () => {
const socket = toSocket(webSocket);
const reader = new WebSocketMessageReader(socket);
const writer = new WebSocketMessageWriter(socket);
languageClient = createLanguageClient({
reader,
writer
});
await languageClient.start();
reader.onClose(() => languageClient.stop());
};
return webSocket;
};
const createLanguageClient = (transports: MessageTransports): MonacoLanguageClient => {
return new MonacoLanguageClient({
name: 'Pyright Language Client',
clientOptions: {
documentSelector: [languageId],
errorHandler: {
error: () => ({ action: ErrorAction.Continue }),
closed: () => ({ action: CloseAction.DoNotRestart })
},
workspaceFolder: {
index: 0,
name: 'workspace',
uri: monaco.Uri.parse('/tmp')
},
synchronize: {
fileEvents: [vscode.workspace.createFileSystemWatcher('**')]
}
},
connectionProvider: {
get: () => {
return Promise.resolve(transports);
}
}
});
};
Next, you need to create a virtual file system as the input and output of the Monaco Editor instance.
const fileSystemProvider = new RegisteredFileSystemProvider(false);
fileSystemProvider.registerFile(new RegisteredMemoryFile(vscode.Uri.file('/test.py'), 'print("Hello, laf!")'));
registerFileSystemOverlay(1, fileSystemProvider);
const modelRef = await createModelReference(monaco.Uri.file('/test.py'));
Finally, create a Monaco Editor instance and perform some configuration.
createConfiguredEditor(document.getElementById('container')!, {
model: modelRef.object.textEditorModel,
automaticLayout: true,
minimap: {
enabled: false
},
scrollbar: {
verticalScrollbarSize: 4,
horizontalScrollbarSize: 8,
},
overviewRulerLanes: 0,
lineNumbersMinChars: 4,
scrollBeyondLastLine: false,
theme: 'vs',
});
In this way we have completed the front end.
import Editor from './python/Editor';
function App() {
return (
<>
<h2>monaco python lsp</h2>
<div style={
{height:"500px", width:"800px", border:"1px solid black", padding:"8px 0"}}>
<Editor />
</div>
</>
)
}
export default App
The effect is as follows
summary
It is still very difficult to deeply understand LSP and the working principles behind it, but fortunately there are excellent open source projects such as languageserver and languageclient to provide support, which allows us to have good code editing after just piecing together a few pieces of code. device effect. The next step is to use LSP to transform Laf 's Web IDE.
Since I have just come into contact with this piece of knowledge, it is inevitable that there will be errors and omissions in the article. I hope to communicate and make progress with everyone who reads this.
References
- https://github.com/microsoft/language-server-protocol/wiki/Protocol-History
- https://medium.com/@malintha1996/understanding-the-language-server-protocol-5c0ba3ac83d2
- https://ubug.io/blog/workpad-part-6
- https://www.typefox.io/blog/how-to-embed-a-monaco-editor-in-a-browser-as-a-part-of-my-first-task-at-typefox
- https://www.typefox.io/blog/teaching-the-language-server-protocol-to-microsofts-monaco-editor