Synchronization
We’re moving ahead here, we have round trips working between our client and server, we’ve communicated capabilities acknowledged each others strengths and weaknesses and are now in a healthy relationship… At least our server and client are.
Next up, we’re going to start talking about synchronization.
How is it that when you change a document a letter on a variable for example, you all of a sudden get a red squiggly line under it?
Well the answer is somewhat obvious, our editors are constantly communicating based on events that often we are triggering.
To start receiving messages based on these events is actually quite simple.
When we created initialize.go file, we created a empty type called ServerCapabilities to add capabilities to at a later point.
Well now is time to do that so, the first capability we want to inform the client of is textDocumentSync.
So in our initialize.go file, we’re going to add the following:
type ServerCapabilities struct { TextDocumentSync int `json:"textDocumentSync"`}For the value of textDocumentSync lets check the documentation:
We see that we need to tell the client what type of textDocumentSync we support and for
our purposes we’re going to support Full changes,
since we won’t be getting into advanced diffing and patching of documents this evening.
So lets update our helper function to set the appropriate value.
func NewInitializeResponse(id int) InitializeResponse { return InitializeResponse{ Response: Response{ Id: &id, Jsonrpc: "2.0", }, Result: InitializeResult{ Capabilities: ServerCapabilities{ TextDocumentSync: 1, }, // what we're able to do ServerInfo: ServerInfo{ Name: "demo_lsp", // name of lsp Version: "0.0.0.0", // version of lsp }, }, }}Seeing the textDocumentSync in action
After rebuilding and restarting our server, we can now see that not only is the client aware of the textDocumentSync capability,
it has sends us a new message every time an event like save, change, or open happens.
{"method":"textDocument\/didOpen","jsonrpc":"2.0","params":{"textDocument":{"languageId":"markdown","version":0,"uri":"file:\/\/\/Users\/michaelduren\/Code\/learning\/go-meetups\/demo-proj\/test.md","text":"# This is a test file\n\nHello\nHey\n"}}}[demo_lsp]2024/06/19 11:32:53 main.go:31: Message received: Content-Length: 165
{"method":"textDocument\/didSave","jsonrpc":"2.0","params":{"textDocument":{"uri":"file:\/\/\/Users\/michaelduren\/Code\/learning\/go-meetups\/demo-proj\/test.md"}}}[demo_lsp]2024/06/19 11:32:53 main.go:31: Message received: Content-Length: 249
{"method":"textDocument\/didChange","jsonrpc":"2.0","params":{"textDocument":{"version":3,"uri":"file:\/\/\/Users\/michaelduren\/Code\/learning\/go-meetups\/demo-proj\/test.md"},"contentChanges":[{"text":"# This is a test file\n\nHello\n\nHey\n"}]}}[demo_lsp]2024/06/19 11:32:54 main.go:31: Message received: Content-Length: 165
{"method":"textDocument\/didSave","jsonrpc":"2.0","params":{"textDocument":{"uri":"file:\/\/\/Users\/michaelduren\/Code\/learning\/go-meetups\/demo-proj\/test.md"}}}[demo_lsp]2024/06/19 11:32:55 main.go:31: Message received: Content-Length: 251
{"method":"textDocument\/didChange","jsonrpc":"2.0","params":{"textDocument":{"version":5,"uri":"file:\/\/\/Users\/michaelduren\/Code\/learning\/go-meetups\/demo-proj\/test.md"},"contentChanges":[{"text":"# This is a test file\n\nHello\n\nHey\n\n"}]}}[demo_lsp]2024/06/19 11:32:56 main.go:31: Message received: Content-Length: 251
{"method":"textDocument\/didChange","jsonrpc":"2.0","params":{"textDocument":{"version":6,"uri":"file:\/\/\/Users\/michaelduren\/Code\/learning\/go-meetups\/demo-proj\/test.md"},"contentChanges":[{"text":"# This is a test file\n\nHello\n\nHey\n\n"}]}}[demo_lsp]2024/06/19 11:32:56 main.go:31: Message received: Content-Length: 249
{"method":"textDocument\/didChange","jsonrpc":"2.0","params":{"textDocument":{"version":7,"uri":"file:\/\/\/Users\/michaelduren\/Code\/learning\/go-meetups\/demo-proj\/test.md"},"contentChanges":[{"text":"# This is a test file\n\nHello\n\nHey\n"}]}}[demo_lsp]2024/06/19 11:32:56 main.go:31: Message received: Content-Length: 165
{"method":"textDocument\/didSave","jsonrpc":"2.0","params":{"textDocument":{"uri":"file:\/\/\/Users\/michaelduren\/Code\/learning\/go-meetups\/demo-proj\/test.md"}}}Storing the document state
Next we need to start handling these messages and updating our internal state accordingly, if you remember when we applied textDocumentSync in our capabilities we set it to 1 which means that we support full text synchronization,
so we will get the entire document each time which isn’t very good from a performance perspective but in this demonstration it’s fine.
To do this we’re going to create a new directory called thesauraus (you’ll see why in a moment) and a state.go file.
mkdir thesauraus && touch thesauraus/state.gopackage thesauraus
type State struct { // Filenames to contents Documents map[string]string}
func NewState() State { return State{ Documents: make(map[string]string), }}
func (s *State) OpenDocument(uri, text string) { s.Documents[uri] = text}
func (s *State) UpdateDocument(uri, text string) { s.Documents[uri] = text}Here we can see a very basic in which we use a map with a URI as a key and the files contents as the value.
Next we need to create a type to represent a textdocument and the types for the textDocument/didOpen and textDocument/didChange messages.
As usual we can check out the documentation.
Although the types are below:
touch lsp/textdocument.go lsp/textdocument_didopen.go lsp/textdocument_didchange.gopackage lsp
type TextDocumenItem struct { Uri string `json:"uri"` LanguageId string `json:"languageId"` Text string `json:"text"` Version int `json:"version"`}
type Position struct { Line int `json:"line"` Character int `json:"character"`}
type TextDocumentIdentifier struct { Uri string `json:"uri"`}
type TextDocumentPositionParams struct { TextDocument TextDocumentIdentifier `json:"textDocument"` Position Position `json:"position"`}
type VersionedTextDocumentIdentifier struct { TextDocumentIdentifier Version int `json:"version"`}package lsp
type DidOpenTextDocumentNotification struct { Notification Params DidOpenTextDocumentParams `json:"params"`}
type DidOpenTextDocumentParams struct { TextDocument TextDocumenItem `json:"textDocument"`}package lsp
type TextDocumentDidChangeNotification struct { Notification Params DidChangeTextDocumentParams `json:"params"`}
// since this is a notification, we don't have a responsetype DidChangeTextDocumentParams struct { TextDocument VersionedTextDocumentIdentifier `json:"textDocument"` ContentChanges []TextDocumentContentChangeEvent `json:"contentChanges"`}
/** * An event describing a change to a text document. If only a text is provided * it is considered to be the full content of the document. */type TextDocumentContentChangeEvent struct { /** * The new text for the provided range. */ Text string `json:"text"`}Finally it’s time to update the main method in our switch statement and update our state during each client message.
// after initialize method case "textDocument/didOpen": var request lsp.DidOpenTextDocumentNotification
if err := json.Unmarshal(content, &request); err != nil { logger.Printf("textDocument/didOpen: %s\n", err) return }
logger.Printf("Connected to: %s", request.Params.TextDocument.Uri) logger.Printf("Contents: %s", request.Params.TextDocument.Text) state.OpenDocument(request.Params.TextDocument.Uri, request.Params.TextDocument.Text) case "textDocument/didChange": var request lsp.TextDocumentDidChangeNotification
if err := json.Unmarshal(content, &request); err != nil { logger.Printf("textDocument/didChange: %s\n", err) return }
logger.Printf("Changed document: %s", request.Params.TextDocument.Uri) logger.Printf("Changes: %s", request.Params.ContentChanges[0].Text)
for _, change := range request.Params.ContentChanges { // should only be one change state.UpdateDocument(request.Params.TextDocument.Uri, change.Text) }And with that we have a basic synchronization mechanism in place.