CodeMirror 6, a rewrite of the CodeMirror editor, brings several improvements. Toph has been using CodeMirror for its integrated code editor since its introduction.
As CodeMirror 6 reached a stable interface with the promise of better touchscreen support, it was time for an upgrade! During which I wanted to introduce language server support.
The goal was to provide code completion, diagnostics, and hover tooltips. And, CodeMirror 6 makes it easy to do all three.
All of these have been packaged into a small library and made available on NPM:
The @codemirror/autocomplete package provides an autocompletion extension.
1
autocompletion(config?: Object = {}) →Extension
CodeMirror already provides a UX for code completion. Given the context of where code completion is activated, you need to provide the options for completion. To do that, configure a completion source through the override property of the autocompletion config object.
autocompletion({
override: [
(context) => {
lettriggered=false; // Set to true if the cursor is positioned right after a trigger character (from server capabilities).
lettriggerCharacter=void0; // If triggered, set this to the trigger character.
if (triggered||context.explicit) {
returnrequestCompletion(context, {
kind:triggered?2:1character:triggerCharacter });
}
}
]
})
classPlugin {
// ...
requestCompletion(context, trigger) {
this.sendChange(/* ... */);
returnthis.client.request({
method:'textDocument/completion',
params: { /* ... */ }
}, timeout).then((result) => {
letcompletions; // Transform result.items to CodeMirror's Completion objects.
returncompletions;
});
}
// ...
}
The requestCompletion function should return a Promise of CompletionResult.
Diagnostics
You can show diagnostics in CodeMirror by dispatching setDiagnostics() with the current state and an array of Diagnostic objects:
From within the plugin’s update method, you can determine whether the document has changed. Ideally, some debouncing behavior should be implemented to send changes to the language server only when the editor is idle (user has stopped typing and a small duration has elapsed):
The drill here is simple: Once the source function is called by CodeMirror (which happens when the mouse cursor is hovering a bit of code), send a request to the language server and wait for it to respond with relevant documentation.
Language Servers speak JSON-RPC 2.0 over standard IO. To invoke a method or send a notification to a language server, you can write to the process’s standard input.
Start a language server (in this example it is the language server for Go) in your terminal:
Notifications are similar, except that you do not expect any response from them.
You can now write a small daemon program that listens for WebSocket connections and spins up a language server when a connection is established.
The program should then read incoming messages from the WebSocket. Since the message will only contain the payload and not the headers, the program should first write the headers to the standard input of the language server, followed by the payload.
The program should also read from the language server’s standard output, validate the headers before discarding them, and send the payload back through the WebSocket.
A trivial example of the above would look like this:
// Adapted from https://github.com/gorilla/websocket/tree/master/examples/command.
// Error handling omitted for brevity.
funcserveWs(whttp.ResponseWriter, r*http.Request) {
stack:=r.URL.Query().Get("stack")
ws, _:=upgrader.Upgrade(w, r, nil)
deferws.Close()
// Start Language Server inside Docker using locally available tagged images.
cmd:=exec.Command("docker", "run", "-i", "lsp-"+stack)
inw, _:=cmd.StdinPipe()
outr, _:=cmd.StdoutPipe()
cmd.Start()
done:= make(chanstruct{})
gopumpStdout(ws, outr, done) // Read from stdout, write to WebSocket.
goping(ws, done)
pumpStdin(ws, inw) // Read from WebSocket, write to stdin.
// Some commands will exit when stdin is closed.
inw.Close()
// Other commands need a bonk on the head.
cmd.Process.Signal(os.Interrupt)
select {
case<-done:
case<-time.After(time.Second):
// A bigger bonk on the head.
cmd.Process.Signal(os.Kill)
<-done }
cmd.Process.Wait()
}