DEV Community

pickuma
pickuma

Posted on Originally published at pickuma.com

What interactive refactoring in gopls means for Go devs

The Go team has added interactive refactoring to gopls, the official Go language server. The Go Blog announced it in Interactive refactoring in gopls. Refactorings can now pause and ask you for input before they change your code. For example, they can ask which struct tags to add or which interface to implement. What decides who gets this feature is how it is built. It relies on a new capability the Go team is proposing for the Language Server Protocol (microsoft/language-server-protocol#1164). Working support currently ships only in gopls v0.24.0 and the official VS Code Go extension v0.56.0.

The background you need

The Language Server Protocol (LSP) is an open standard that separates language knowledge from the editor. Your editor (VS Code, Neovim, Emacs) handles the text and the UI. A separate program, the language server, understands the language. For Go, that server is gopls. The two exchange messages constantly over JSON-RPC, a simple format for sending requests and responses as JSON. Those messages power autocomplete, hover docs, diagnostics, and code actions.

The benefit is reuse. Write one Go server, and every editor that speaks LSP gets Go support. Improve the server, and every editor gets the improvement.

A code action is an entry in your editor's lightbulb or quick-fix menu, such as extract function, fill struct, or add tags. You pick one, the server computes the text edits, and the editor applies them.

The Go Blog describes the limitation. Almost every code transformation in LSP is, in its words, "like a function of no parameters". You trigger it, and the server immediately returns edits. The server has to guess what you want from context alone: where your cursor sits and the surrounding syntax tree. Rename is the one exception. It takes a single input, the new name, and it needed its own dedicated request in the protocol, PrepareRename.

Think of a vending machine with one button per item. Press Add struct tags and you get exactly one product. You cannot choose which tags or what casing. The analogy breaks in one important way: the machine was never short of stock. According to the Go Blog, gopls could always compute the right edits. The problem was that LSP gave the server no standard way to ask you a question. When context was not enough, the server had two choices. It could apply a default, or it could not offer the refactoring at all.

Two pieces of Go background help here:

  • Struct tags are the backtick strings after struct fields, like json:"first_name". Libraries read them to decide how a field is named when encoding to JSON, writing to a database, and so on. Different libraries want different keys and different casing.
  • Go interfaces are satisfied implicitly. A type implements an interface just by having the right methods. There is no implements keyword, so writing every method stub by hand is tedious.

What actually changed

This was already true before the release:

  • gopls offered Add struct tags, but according to the Go Blog it always produced json tags in camelCase.
  • Remove struct tags removed every tag instead of letting you choose which ones.
  • To stub out an interface, you copied the interface by hand. Or you used an external tool such as impl, typing the receiver and the qualified interface name yourself.
  • Some language extensions worked around the LSP limitation by building custom dialogs into specific editors. Editors that stick strictly to the LSP, like Neovim and Emacs, did not get those dialogs.

This is new:

  • The Go team, working with the Dart team, has proposed a standard LSP mechanism that lets a server collect user input before it computes edits. It supports multi-step dialogs. It is designed for any language and editor, not just Go.
  • gopls v0.24.0 and vscode-go v0.56.0 implement it.
  • Add struct tags now asks which tags to add and which naming convention to use. The blog's example adds both json and db tags in snake_case.
  • Remove struct tags now asks which keys to delete. You can drop db tags and keep json tags.
  • Add methods to implement an interface... lets you pick the interface. It then generates stub methods whose bodies call panic("unimplemented") until you fill them in.
  • A Slay the dragon code action appears when your cursor is on a package-level variable named dragonSlayer. It demonstrates multi-step dialogs and gives editor developers a self-contained test case.

This has not shipped yet: moving declarations across files and packages. The Go Blog calls it a preview scheduled for an upcoming gopls release. In the blog's example, type Foo moves to another package. Its method moves with it, and so does an unexported constant that only Foo uses. An unrelated function stays where it was, and the imports in both files are updated.

What it means for you

The most important fact is that this is an LSP capability, not a VS Code feature. That has consequences in both directions.

If you use VS Code, you can try it now. Update the Go extension to v0.56.0 or later, and make sure gopls is v0.24.0 or later. Then open the code actions menu on a struct or type and look for the new dialogs. If you are used to the old Add struct tags behavior, expect a prompt now instead of instant camelCase json tags.

If you use Neovim, Emacs, or another LSP client, gopls is ready, but your editor must also support the new protocol messages. According to the Go Blog, client integrations for Neovim and Emacs have only been explored as proofs of concept. Watch your LSP client plugin's changelog. The dragonSlayer action exists so client authors can test their implementation. You can use it the same way to check whether your setup handles the dialogs.

If you use other languages, this could reach you too. The Dart team helped design the mechanism, and the stated goal is that any language server can use it. With a standard way to ask questions, server authors would no longer need editor-specific workarounds to offer refactorings that take input. That matters most to users of editors that rely on plain LSP.

What to watch next:

  • The status of microsoft/language-server-protocol#1164. It is a proposal, not part of the published spec.
  • The gopls release that ships moving declarations.
  • Support in your own editor's LSP client.

Moving declarations across files and packages is not in gopls v0.24.0. The Go Blog shows a working implementation, but it calls the feature a preview scheduled for an upcoming release. Until that release, moving code between packages still means cutting, pasting, and fixing imports by hand.

What we don't know yet

  • Whether LSP will adopt the proposal, and in what form. The Go Blog describes it as a capability "we are proposing". The sources do not say how the LSP maintainers have responded, or whether the message format might change before it becomes standard.
  • What happens in editors without support. The post does not say whether gopls hides these refactorings, falls back to the old defaults, or does something else when the editor cannot show a dialog.
  • Timing. The post gives no date for the moving-declarations release or for production-ready Neovim and Emacs support.
  • The roadmap. The post says the capability opens several new directions for gopls, but the material we have does not describe them.
  • Other language servers. Apart from the Dart team's role in the design, the sources do not say which other language servers plan to implement the mechanism.

Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.

Top comments (0)