Introducing the Buf Language Server

Aug 19, 2022/4 min read

At Buf, we sometimes experiment to figure out how we can best help move the Protobuf community forward. Sometimes these experiments don't really go anywhere, but other times they turn into real products. Traditionally, we've kept these experiments internal until they're production-ready, but we'd like to start sharing more of these experiments with the community, even if they don't end up going anywhere. Today we're excited to publicize one of these experiments - the beginnings of a Protobuf language server that speaks the standard Language Server Protocol (LSP) available at github.com/bufbuild/buf-language-server.

The bufls language server is a proof of concept - we don't actively maintain this, and there are no guarantees in terms of stability, but we want to hear your feedback! Please give bufls a try, message us on Slack, and let us know what you think!

LSP

The LSP was designed to standardize IDE functionality behind a common protocol. Put simply, implementing a language server enables seamless integration between many IDEs all at once. There are a variety of server implementations available today for a number of programming languages, including JavaScript, TypeScript, Go, and Rust. There are also a number of tools that support the LSP as clients, including Vim, VSCode, and Sublime.

Usage

You can install the bufls command (short for Buf Language Server) with the following command:

$ go install github.com/bufbuild/buf-language-server/cmd/bufls@latest

Now, all you need to do is wire up the language server with your LSP-compatible tool, such as Vim. If you use vim-lsp, you only need to configure the following:

Plug 'prabirshrestha/vim-lsp'

augroup LspBuf
  au!
  autocmd User lsp_setup call lsp#register_server({
      \ 'name': 'bufls',
      \ 'cmd': {server_info->['bufls', 'serve']},
      \ 'whitelist': ['proto'],
      \ })
  autocmd FileType proto nmap <buffer> gd <plug>(lsp-definition)
augroup END

Go to definition

The bufls server only supports the textDocument/definition endpoint for now (hence the lsp-definition configuration shown above). This means that you can jump to the definition of the Protobuf descriptor that your cursor is hovering over.

For example, consider the following object.proto file, where the surrounding [ ] represents the current cursor position:

syntax = "proto3";

package acme.object.v1;

message Object {
  string id = 1;
}

message GetObjectRequest {
  string id = 1;
}

message GetObjectResponse {
  [O]bject object = 1;
}

service ObjectService {
  rpc GetObject(GetObjectRequest) returns (GetObjectResponse);
}

If you issue the textDocument/definition command from your editor, the cursor will jump to the definition of the acme.object.v1.Object descriptor and your editor will be updated like so:

syntax = "proto3";

package acme.object.v1;

message [O]bject {
  string id = 1;
}

message GetObjectRequest {
  string id = 1;
}

message GetObjectResponse {
  Object object = 1;
}

service ObjectService {
  rpc GetObject(GetObjectRequest) returns (GetObjectResponse);
}

Where things get really interesting is when your cursor is on an identifier that is defined in another file, package, or module. In fact, the textDocument/definition endpoint adopts the same semantics as the buf CLI so that references are resolved to the descriptors defined in your workspace (defined by a buf.work.yaml), or the module cache for dependencies included in your buf.lock manifest.

A tool like this is tremendously helpful for understanding exactly where a descriptor is defined - it's not always clear where descriptors are resolved from import statements alone, especially if multiple files from the same package are imported like so:

syntax = "proto3";

package acme.object.v1;

import "acme/pkg/v1/caller.proto";
import "acme/pkg/v1/types.proto";

message Object {
  string id = 1;
}

message GetObjectRequest {
  string id = 1;
  acme.pkg.v1.Context context = 2;
}

message GetObjectResponse {
  Object object = 1;
  acme.pkg.v1.Metadata metadata = 2;
}

service ObjectService {
  rpc GetObject(GetObjectRequest) returns (GetObjectResponse);
}

From this view, it's impossible to know where the acme.pkg.v1.{Context,Metadata} descriptors are defined without looking into the caller.proto or types.proto files yourself. But with bufls, you can find out exactly where the type is defined instantly (just like you're used to with your programming language of choice).

Talk to us

Our goal here is to get your feedback. Reach out to us on Slack - we'd love to hear from you!

Get started for free with the Buf Schema Registry