2024-04-25
Making an LSP server in Go
Resources and documentation about implementing an LSP server in Go are surprisingly sparse, so I decided to write my (first!) blog entry about it, after having to go through the source code of some random dependent on go.lsp.dev/protocol to figure out how to implement a simple LSP server.
The pieces
We’ll be using the go.lsp.dev collection of modules to implement our LSP server. These modules include:
- go.lsp.dev/protocol: A bunch of structs that represent the LSP protocol.
- go.lsp.dev/jsonrpc2: A JSON-RPC 2.0 implementation. The LSP protocol uses JSON-RPC 2.0 for communication.
The boilerplate
You’ll most likely want to split up your code into at least two files:
server.go
: Logic (and honestly, mostly boilerplate) for starting the LSP servercmd/main.go
: The entrypoint for launching your server with a binary. In the simplest of cases, you might not even need this. If you don’t, put every file in themain
package, and just callStartServer
(see just below) from amain
function inserver.go
.handlers.go
: The actual LSP handlers, you’ll define all the functions that you want to implement there! things likeDefinition
to implement the “Go to definition” feature, etc.
server.go
Mostly boilerplate:
package yourlsp
import (
"context"
"io"
"os"
"path/filepath"
"go.lsp.dev/jsonrpc2"
"go.lsp.dev/protocol"
"go.uber.org/multierr"
"go.uber.org/zap"
)
// StartServer starts the language server.
// It reads from stdin and writes to stdout.
func StartServer(logger *zap.Logger) {
conn := jsonrpc2.NewConn(jsonrpc2.NewStream(&readWriteCloser{
reader: os.Stdin,
writer: os.Stdout,
}))
handler, ctx, err := NewHandler(
context.Background(),
protocol.ServerDispatcher(conn, logger),
logger,
)
if err != nil {
logger.Sugar().Fatalf("while initializing handler: %w", err)
}
conn.Go(ctx, protocol.ServerHandler(
handler, jsonrpc2.MethodNotFoundHandler,
))
<-conn.Done()
}
type readWriteCloser struct {
reader io.ReadCloser
writer io.WriteCloser
}
func (r *readWriteCloser) Read(b []byte) (int, error) {
n, err := r.reader.Read(b)
return n, err
}
func (r *readWriteCloser) Write(b []byte) (int, error) {
return r.writer.Write(b)
}
func (r *readWriteCloser) Close() error {
return multierr.Append(r.reader.Close(), r.writer.Close())
}
cmd/main.go
package main
import (
"go.uber.org/zap"
"yourlsp"
)
func main() {
logger, _ := zap.NewDevelopmentConfig().Build()
// Start the server
yourlsp.StartServer(logger)
}
handlers.go
That’s were we get to the interesting stuff.
You first define your own Handler
struct that just embeds the protocol.Server
struct, so that you can implement all the methods.
package yourlsp
import (
"context"
"go.lsp.dev/protocol"
"go.lsp.dev/uri"
"go.uber.org/zap"
)
var log *zap.Logger
type Handler struct {
protocol.Server
}
func NewHandler(ctx context.Context, server protocol.Server, logger *zap.Logger) (Handler, context.Context, error) {
log = logger
// Do initialization logic here, including
// stuff like setting state variables
// by returning a new context with
// context.WithValue(context, ...)
// instead of just context
return Handler{Server: server}, context, nil
}
Implementing stuff
Actually implementing functionnality for your LSP consists in defining a method on
your handler struct that has a particular name, as defined in theprotocol.Server
interface
Telling the clients what feature we support: the Initialize
method
All LSP servers must implement the Initialize
method, that
returns information on the LSP server, and most importantly, the features it supports.
The signature of the Initialize
method is:
Initialize(ctx context.Context, params *InitializeParams) (result *InitializeResult, err error)
Autocomplete and hover (features of Go’s LSP server, how meta!) will help you a lot when implementing this:
You’ll notice that most of the capabilities receive an interface{}
, that’s not very helpful.
This usually means that you can either pass true
to say that you support the feature fully, or a struct of options for more intricate support information.
Check with the spec to be sure.
Example
Here’s an example method implementation that signals support for the Go to Definition feature:
func (h Handler) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) {
return &protocol.InitializeResult{
Capabilities: protocol.ServerCapabilities{
DefinitionProvider: true, // <-- right there
},
ServerInfo: &protocol.ServerInfo{
Name: "yourls",
Version: "0.1.0",
},
}, nil
}
Implementing a feature: the Definition
example
As with Initialize
, hovering over the types of the parameters will help you greatly.
// IMPORTANT: You _can't_ take a pointer to your handler struct as the receiver,
// your handler will no longer implement protocol.Server if you do that.
func (h Handler) Definition(ctx context.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) {
// ... do your processing ...
return []protocol.Location{
{
URI: uri.File(...),
Range: protocol.Range{
Start: protocol.Position{
Line: 0,
Character: 0,
},
End: protocol.Position{
Line: 0,
Character: 0,
},
},
},
}, nil
}
Using in IDEs & editors
Neovim
You can put the following in your init.lua
:
vim.api.nvim_create_autocmd({'BufEnter', 'BufWinEnter'}, {
pattern = { "glob pattern of the files you want your LSP to be used on" },
callback = function(event)
vim.lsp.start {
name = "My language",
cmd = {"mylsp"},
}
end
})
Visual Studio Code
VSCode requires writing an entire extension to use an LSP server…
If you want something quick ’n’ dirty, you can use some generic LSP client extension (for example, llllvvuu’s Generic LSP Client).
But to do a proper extension that you can distribute to your user’s, you’ll want to follow the vscode docs on LSP extension development.
The guide assumes that you’ll develop the LSP server in NodeJS too, but you can easily rm -rf
the hell out of the server/
directory from their template repository.
I’m using the following architecture for ortfo’s LSP server:
handler.go
server.go
cmd/
main.go
vscode/
package.json # contains values from both client's package.json and the root package.json
src/ # from client/
tsconfig.json
...
(curious? see the repository at ortfo/languageserver)
Related works
whoami?
Je suis intéressée par tout ce qui est à la fois créatif et numérique.
Learn more about me