Language Server Protocol (LSP) is a language and editor agnostic protocol for communication between Language Servers (like Solargraph or Sorbet for Ruby) and editors like VS Code or Emacs.

The language server can provide information like autocomplete possibilities, or linting or… all the things you expect from a modern IDE.

The beauty of it is that before LSP, every editor (Vim, Emacs, VS Code, RubyMine, etc.) implemented their own process for giving this intellisense. With LSP, a single server can communicate with any editor that understands the LSP protocol.

This means that when I work on the fledgling Rails plugin for Solargraph, people who use Vim, VS Code, and everyone else can benefit if they’re willing to talk LSP. It doesn’t matter that I care most about Emacs, pick whatever client you like.

The communication between editor and server is surprisingly intelligible, and like all logs really boring unless you’re debugging a problem between client and server. Below is a sample (ignore my comment lines, preceded by #):

** [Trace - 04:45:59 pm] Sending notification 'textDocument/didChange'.
# Editor Notification: (add four spaces to start of line 22)
Params: {
  "textDocument": {
    "uri": "file:///my/rails/project/app/models/book.rb",
    "version": 44
  },
  "contentChanges": [
    {
      "range": {
        "start": {
          "line": 22,
          "character": 0
        },
        "end": {
          "line": 22,
          "character": 0
        }
      },
      "rangeLength": 0,
      "text": "    "
    }
  ]
}

** [Trace - 04:45:21 pm] Sending notification 'textDocument/didSave'.
# Editor Notification: saved the file
Params: {
  "textDocument": {
    "uri": "file:///my/rails/project/app/models/book.rb",
    "version": 43
  }
}

These are extracted from debugging logs on my editor, which omits details that are really important for a protocol description, but really unimportant for understanding the gist. You can see most of the body of these messages: the method (‘textDocument/didSave’) and the params.

The above messages are notifications from the editor, which do not get responses from the server. But there are also Request and Response messages available.

Below is the rest of a conversation made up of:

  • Editor Notification (N): here the editor is just telling the server something
  • Editor request (RQ): the editor wants to know something
  • Server response (RP): the server responds
** [Trace - 04:45:59 pm] Sending request 'textDocument/documentSymbol - (53)'.
# Editor RQ: tell me about this file now
Params: {
  "textDocument": {
    "uri": "file:///my/rails/project/app/models/book.rb"
  }
}
** [Trace - 04:45:59 pm] Received response 'textDocument/documentSymbol - (53)' in 31ms.
# Server RP: here's stuff about this file (constant Book, method other_books_for_same_author)
Result: [
  {
    "name": "Book",
    "containerName": "",
    "kind": 5,
    "location": {
      "uri": "file:///my/rails/project/app/models/book.rb",
      "range": {
        "start": {
          "line": 17,
          "character": 0
        },
        "end": {
          "line": 24,
          "character": 3
        }
      }
    },
    "deprecated": null
  },
  {
    "name": "other_books_for_same_author",
    "containerName": "Book",
    "kind": 6,
    "location": {
      "uri": "file:///my/rails/project/app/models/book.rb",
      "range": {
        "start": {
          "line": 21,
          "character": 2
        },
        "end": {
          "line": 23,
          "character": 5
        }
      }
    },
    "deprecated": null
  }
]

From here on I’m just listing the headings to show the flow of communication while sparing hundreds of lines:

** [Trace - 04:46:01 pm] Sending notification 'textDocument/didChange'.
# Editor N: Added character B starting at (22,4) ending (22,4)
** [Trace - 04:46:02 pm] Sending request 'textDocument/documentSymbol - (54)'.
# Editor RQ: tell me about this file now?
** [Trace - 04:46:02 pm] Received response 'textDocument/documentSymbol - (54)' in 29ms.
# Server RP: here's what I know about this file (contant Book, method etc)
** [Trace - 04:46:02 pm] Sending notification 'textDocument/didChange'.
# Editor N: added an 'o' after the 'b' at (22,5)
** [Trace - 04:46:02 pm] Sending request 'textDocument/completion - (55)'.
# Editor RQ: perform completion for the next character (22,6)
** [Trace - 04:46:02 pm] Received response 'textDocument/completion - (55)' in 8ms.
# Server RP: Here are completion items for position (22,6)
** [Trace - 04:46:03 pm] Sending request 'completionItem/resolve - (57)'.
# Editor RQ: resolve this completion item I chose
** [Trace - 04:46:03 pm] Received response 'completionItem/resolve - (56)' in 27ms.
# Server RP: I resolved this completion item for you
** [Trace - 04:46:04 pm] Sending request 'textDocument/documentSymbol - (58)'.
# Editor RQ: tell me about this file now
** [Trace - 04:46:04 pm] Received response 'textDocument/documentSymbol - (58)' in 20ms.
# Server RP: here's some info about this file (constant Book, method etc)

Notice how much repetition there is. “I changed one character. Now tell me about the file. Ok, here’s the file. I changed another character. Now tell me about the file”.

That’s just computers for you, they’re at their best being really literal and exhaustive (so nothing is misunderstood) and much faster than humans can be at generating or consuming this volume of communication.

If you’re interested in even more details you can find the protocol specification here.