|
|
|
|
@ -3,18 +3,6 @@ title: Writing My Own Language Server in Go (to Parse Chess PGNs)
|
|
|
|
|
publishedTime: 2024-06-26
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
<!--toc:start-->
|
|
|
|
|
|
|
|
|
|
- [What is a Language Server?](#what-is-a-language-server)
|
|
|
|
|
- [What is a Chess PGN?](#what-is-a-chess-pgn)
|
|
|
|
|
- [What can this language server do?](#what-can-this-language-server-do)
|
|
|
|
|
- [Making the Server](#making-the-server)
|
|
|
|
|
- [Notes and Issues](#notes-and-issues)
|
|
|
|
|
- [The Protocol](#the-protocol)
|
|
|
|
|
- [Analysis](#analysis)
|
|
|
|
|
- [Conclusion](#conclusion)
|
|
|
|
|
<!--toc:end-->
|
|
|
|
|
|
|
|
|
|
### What is a Language Server?
|
|
|
|
|
|
|
|
|
|
If you're unfamiliar, the Language Server Protocol is a protocol by which a client (usually a code editor) can talk to a language server and get information about an open file and/or workspace. This is made use of in editors like VS Code and Neovim (my editor of choice, by the way). The protocol passes data around in JSON which essentially allows for an RPC where the client tells the server to do something or vice versa. These can be built in any language, as long as said language supports whatever transport method you choose for the JSON RPC (usually stdout and stdin, but TCP is another example of an option). For this language server I built it in Go using stdout and stdin.
|
|
|
|
|
@ -29,16 +17,15 @@ In its current basic form, it can parse a PGN of a single chess game, report err
|
|
|
|
|
|
|
|
|
|
## Making the Server
|
|
|
|
|
|
|
|
|
|
### Notes and Issues
|
|
|
|
|
|
|
|
|
|
### Notes and Issues
|
|
|
|
|
Since this was my first time ever creating a language server, I no doubt made some mistakes, and I'm also fairly new to Go, so I have no doubt that will have contributed to any issues as well, but all that being said, this went over pretty painlessly. I had some hiccups with the parser itself, but as far as implementing the protocol it went off without much of a hitch. The only real warning I'd give to people following suit on this is that you should probably design your analysis tool before doing much else because the way I did it, I felt a lot like I was jumping around my codebase continuously adding and removing from the LSP to fit the demands of the analysis tool. That might seem self-evident, especially if you're writing a language server for a programming language, but I figured I should mention it.
|
|
|
|
|
|
|
|
|
|
### The Protocol
|
|
|
|
|
|
|
|
|
|
I started with the help of TJ Devries' [educationalsp repo](https://github.com/tjdevries/educationalsp) creating some helper functions for the RPC:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// rpc/rpc.go
|
|
|
|
|
//rpc/rpc.go
|
|
|
|
|
package rpc
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
@ -105,9 +92,8 @@ func Split(data []byte, _ bool) (advance int, token []byte, err error) {
|
|
|
|
|
return totalLength, data[:totalLength], nil
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// main.go
|
|
|
|
|
//main.go
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
@ -159,7 +145,6 @@ func writeResponse(w io.Writer, msg any) {
|
|
|
|
|
The `main.go` code has been truncated to only show the initialize event, but I left in the bits of it (i.e. the `state` parameter of the `handleMessage` function that won't become clear just yet).
|
|
|
|
|
If you don't fully understand the code in `rpc.go`, I recommend watching the beginning portion of [TJ's video about the LSP spec](https://youtu.be/YsdlcQoHqPY?si=Sq9mlljv4PgBLpMI).
|
|
|
|
|
The real meat of this, however, comes in the analysis portion, as that's where everything actually happens. Before we look at that though, let's look at some of the implementations for the actual Language Server Protocol's JSON messages.
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// lsp/message.go
|
|
|
|
|
package lsp
|
|
|
|
|
@ -180,27 +165,21 @@ type Notification struct {
|
|
|
|
|
Method string `json:"method"`
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
These are the basic structures for the types of messages, an example of which is the `DidOpenTextDocumentNotification`:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
type DidOpenTextDocumentNotification struct {
|
|
|
|
|
Notification
|
|
|
|
|
Params DidOpenTextDocumentParams `json:"params"`
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Since Go doesn't have traditional inheritance, the `Notification` is just passed as a field in this struct.
|
|
|
|
|
The `Params` are then a type of another struct `DidOpenTextDocumentParams`:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
type DidOpenTextDocumentParams struct {
|
|
|
|
|
TextDocument TextDocumentItem `json:"textDocument"`
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The `TextDocumentItem` struct is what describes the actual file:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
type TextDocumentItem struct {
|
|
|
|
|
URI string `json:"uri"`
|
|
|
|
|
@ -209,10 +188,8 @@ type TextDocumentItem struct {
|
|
|
|
|
Text string `json:"text"`
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
There are other similar structs that are used for other events, but this `Notification` is sent when a file is, shocker, opened.
|
|
|
|
|
As an LSP is expanded you can add more and more of these. The most important `Request` and `Response`, however, are the ones for the initialize event. These define what both the client and server are capable of. The `IntializeResponse` can be viewed here:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
type InitializeResponse struct {
|
|
|
|
|
Response
|
|
|
|
|
@ -223,32 +200,26 @@ type InitializeResult struct {
|
|
|
|
|
ServerInfo ServerInfo `json:"serverInfo"`
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
When the response is given, we give the client these `ServerCapabilities`:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
Capabilities:
|
|
|
|
|
ServerCapabilities{
|
|
|
|
|
Capabilities: ServerCapabilities{
|
|
|
|
|
TextDocumentSync: 2,
|
|
|
|
|
CompletionProvider: map[string]any{},
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Analysis
|
|
|
|
|
|
|
|
|
|
When actually doing the analysis I went through three stages:
|
|
|
|
|
|
|
|
|
|
1. I will use someone's existing PGN parser
|
|
|
|
|
2. I'm having some trouble making use of these existing parsers, I'm going to design one myself
|
|
|
|
|
3. Man, just writing that lexer was a lot, I should try someone else's parser again.
|
|
|
|
|
The one that stuck was one from 5 years ago [by malbrecht](https://github.com/malbrecht/chess), but there are multiple others that exist, and if I continue working on this, I'll probably go back to building my own as I require more customizability. For now though, this parser works out just fine.
|
|
|
|
|
The two features I wanted for this were diagnostics and completions, and this parser (obviously) returns errors if it can't parse the PGN, and is part of a larger package that allows for looking up legal moves from a position.
|
|
|
|
|
The actual analysis tool runs by having a `State` struct that stores the text documents (in this case just the one) and a database of PGNs from the parser (in this case just the one). There are then some functions that are called every time an event happens, like opening a document, updating a document, or asking for completions. I won't put any actual code here, but I'll run you through the basic order in which things happen when running the LSP in the editor.
|
|
|
|
|
4. The Initialize Request is sent, the server responds telling info about the server and its capabilities.
|
|
|
|
|
5. The `textDocument/didOpen` notification is sent, and the server loads the text of the document into the state of the analysis tool. This also sends back any diagnostic information the parser returns.
|
|
|
|
|
6. If the user makes any changes to the PGN, the `textDocument/didChange` notification is sent, and the server loads the text of the document in the state changes to reflect these changes. This also updates the diagnostic information.
|
|
|
|
|
7. Presumably, these changes cause a completion request to be sent, and the server looks through the legal moves at the position at the cursor in the file, and returns them as a list of completion items.
|
|
|
|
|
The one that stuck was one from 5 years ago [by malbrecht](https://github.com/malbrecht/chess), but there are multiple others that exist, and if I continue working on this, I'll probably go back to building my own as I require more customizability. For now though, this parser works out just fine.
|
|
|
|
|
The two features I wanted for this were diagnostics and completions, and this parser (obviously) returns errors if it can't parse the PGN, and is part of a larger package that allows for looking up legal moves from a position.
|
|
|
|
|
The actual analysis tool runs by having a `State` struct that stores the text documents (in this case just the one) and a database of PGNs from the parser (in this case just the one). There are then some functions that are called every time an event happens, like opening a document, updating a document, or asking for completions. I won't put any actual code here, but I'll run you through the basic order in which things happen when running the LSP in the editor.
|
|
|
|
|
1. The Initialize Request is sent, the server responds telling info about the server and its capabilities.
|
|
|
|
|
2. The `textDocument/didOpen` notification is sent, and the server loads the text of the document into the state of the analysis tool. This also sends back any diagnostic information the parser returns.
|
|
|
|
|
3. If the user makes any changes to the PGN, the `textDocument/didChange` notification is sent, and the server loads the text of the document in the state changes to reflect these changes. This also updates the diagnostic information.
|
|
|
|
|
4. Presumably, these changes cause a completion request to be sent, and the server looks through the legal moves at the position at the cursor in the file, and returns them as a list of completion items.
|
|
|
|
|
|
|
|
|
|
## Conclusion
|
|
|
|
|
|
|
|
|
|
Hopefully this was a cool look into what the LSP can do for you, and why it's such a cool technology. I didn't go into a whole lot of detail here, but if you want to take a look at the code for this, it's all on [GitHub](https://github.com/sammyshear/chesslsp). Feel free to make issues or pull requests if you want, as there are definitely problems, I just haven't found them yet.
|
|
|
|
|
|