···11+---
22+date: 2024-04-25
33+title: Making an LSP server in Go
44+works: [ortfo, hyprlang-lsp]
55+---
66+77+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](https://pkg.go.dev/go.lsp.dev/protocol) to figure out how to implement a simple LSP server.
88+99+## The pieces
1010+1111+We'll be using the [go.lsp.dev](https://go.lsp.dev) collection of modules to implement our LSP server. These modules include:
1212+1313+- [go.lsp.dev/protocol](https://pkg.go.dev/go.lsp.dev/protocol): A bunch of structs that represent the LSP protocol.
1414+- [go.lsp.dev/jsonrpc2](https://pkg.go.dev/go.lsp.dev/jsonrpc2): A JSON-RPC 2.0 implementation. The LSP protocol uses JSON-RPC 2.0 for communication.
1515+1616+## The boilerplate
1717+1818+You'll most likely want to split up your code into _at least_ two files:
1919+2020+- `server.go`: Logic (and honestly, mostly boilerplate) for starting the LSP server
2121+- `cmd/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 the `main` package, and just call `StartServer` (see just below) from a `main` function in `server.go`.
2222+- `handlers.go`: The actual LSP handlers, you'll define all the functions that you want to implement there! things like `Definition` to implement the "Go to definition" feature, etc.
2323+2424+### `server.go`
2525+2626+Mostly boilerplate:
2727+2828+```go
2929+package yourlsp
3030+3131+import (
3232+ "context"
3333+ "io"
3434+ "os"
3535+ "path/filepath"
3636+3737+ "go.lsp.dev/jsonrpc2"
3838+ "go.lsp.dev/protocol"
3939+ "go.uber.org/multierr"
4040+ "go.uber.org/zap"
4141+)
4242+4343+4444+4545+// StartServer starts the language server.
4646+// It reads from stdin and writes to stdout.
4747+func StartServer(logger *zap.Logger) {
4848+ conn := jsonrpc2.NewConn(jsonrpc2.NewStream(&readWriteCloser{
4949+ reader: os.Stdin,
5050+ writer: os.Stdout,
5151+ }))
5252+5353+ handler, ctx, err := NewHandler(
5454+ context.Background(),
5555+ protocol.ServerDispatcher(conn, logger),
5656+ logger,
5757+ )
5858+5959+ if err != nil {
6060+ logger.Sugar().Fatalf("while initializing handler: %w", err)
6161+ }
6262+6363+ conn.Go(ctx, protocol.ServerHandler(
6464+ handler, jsonrpc2.MethodNotFoundHandler,
6565+ ))
6666+ <-conn.Done()
6767+}
6868+6969+type readWriteCloser struct {
7070+ reader io.ReadCloser
7171+ writer io.WriteCloser
7272+}
7373+7474+func (r *readWriteCloser) Read(b []byte) (int, error) {
7575+ n, err := r.reader.Read(b)
7676+ return n, err
7777+}
7878+7979+func (r *readWriteCloser) Write(b []byte) (int, error) {
8080+ return r.writer.Write(b)
8181+}
8282+8383+func (r *readWriteCloser) Close() error {
8484+ return multierr.Append(r.reader.Close(), r.writer.Close())
8585+}
8686+8787+8888+```
8989+9090+### `cmd/main.go`
9191+9292+```go
9393+package main
9494+9595+import (
9696+ "go.uber.org/zap"
9797+ "yourlsp"
9898+)
9999+100100+func main() {
101101+ logger, _ := zap.NewDevelopmentConfig().Build()
102102+103103+ // Start the server
104104+ yourlsp.StartServer(logger)
105105+}
106106+```
107107+108108+### `handlers.go`
109109+110110+That's were we get to the interesting stuff.
111111+112112+You first define your own `Handler` struct that just embeds the `protocol.Server` struct, so that you can implement all the methods.
113113+114114+```go
115115+package yourlsp
116116+117117+import (
118118+ "context"
119119+ "go.lsp.dev/protocol"
120120+ "go.uber.org/zap"
121121+)
122122+123123+var log *zap.Logger
124124+125125+type Handler struct {
126126+ protocol.Server
127127+}
128128+129129+func NewHandler(ctx context.Context, server protocol.Server, logger *zap.Logger) (Handler, context.Context, error) {
130130+ log = logger
131131+ // Do initialization logic here, including
132132+ // stuff like setting state variables
133133+ // by returning a new context with
134134+ // context.WithValue(context, ...)
135135+ // instead of just context
136136+ return Handler{Server: server}, context, nil
137137+}
138138+```
139139+140140+## Implementing stuff
141141+142142+Actually implementing functionnality for your LSP consists in defining a method on
143143+your handler struct that has a particular name, as defined in the
144144+[`protocol.Server` interface](https://pkg.go.dev/go.lsp.dev/protocol#Server)
145145+146146+### Telling the clients what feature we support: the `Initialize` method
147147+148148+All LSP servers must implement the `Initialize` method, that
149149+returns information on the LSP server, and most importantly, the features it supports.
150150+151151+The signature of the `Initialize` method is:
152152+153153+```go
154154+Initialize(ctx context.Context, params *InitializeParams) (result *InitializeResult, err error)
155155+```
156156+157157+Autocomplete and hover (features of Go's LSP server, how meta!)
158158+will help you a lot when implementing this:
159159+160160+
161161+162162+You'll notice that most of the _capabilities_ receive an `interface{}`, that's not very helpful.
163163+164164+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.
165165+166166+Check with [the spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#serverCapabilities) to be sure.
167167+168168+### Implementing a feature: the `Definition` example
169169+170170+## Using in IDEs & editors
171171+172172+### Neovim
173173+174174+You can put the following in your `init.lua`:
175175+176176+```lua
177177+vim.api.nvim_create_autocmd({'BufEnter', 'BufWinEnter'}, {
178178+ pattern = { "glob pattern of the files you want your LSP to be used on" },
179179+ callback = function(event)
180180+ vim.lsp.start {
181181+ name = "My language",
182182+ cmd = {"mylsp"},
183183+ }
184184+ end
185185+})
186186+```
187187+188188+### Visual Studio Code
189189+190190+VSCode requires writing an entire extension to use an LSP server...
191191+192192+If you want something quick 'n' dirty, you can use some generic
193193+LSP client extension (for example, [llllvvuu's Generic LSP Client](https://marketplace.visualstudio.com/items?itemName=llllvvuu.llllvvuu-glspc)).
194194+195195+But to do a proper extension that you can distribute to your user's,
196196+you'll want to follow [the vscode docs on LSP extension development](https://code.visualstudio.com/api/language-extensions/language-server-extension-guide).
197197+198198+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.
199199+200200+I'm using the following architecture for [ortfo](https://ortfo.org)'s LSP server:
201201+202202+```tree
203203+handler.go
204204+server.go
205205+cmd/
206206+ main.go
207207+vscode/
208208+ package.json # contains values from both client's package.json and the root package.json
209209+ src/ # from client/
210210+ tsconfig.json
211211+ ...
212212+```
213213+214214+(curious? see [the repository at ortfo/languageserver](https://github.com/ortfo/languageserver))