When editing code in a modern IDE or editor, developers rely on its refactoring features to automate common transformations. Today, we are excited to introduce interactive refactoring in gopls, powered by a new capability we are proposing for the Language Server Protocol (LSP) that brings dynamic, multi-step dialogs directly into your favorite editor. Crucially, we designed this capability (in collaboration with the Dart team) not just for Go, but as an open standard for all programming languages and editor ecosystems. Working support is already available in gopls v0.24.0 and the official Go extension for VS Code v0.56.0.
The LSP is an open standard that decouples language-specific features from specific text editors. As you navigate and edit code, your editor (such as VS Code, Neovim, or Emacs) and a language server (like gopls) engage in a continuous JSON-RPC exchange to provide features like autocomplete, hover documentation, diagnostics, and code actions.
Today, almost every code transformation in the LSP is like a function of no
parameters: you trigger the operation, and the server immediately computes the
edits. (Renaming a symbol is the sole exception: because it is a function of one
parameter, the desired new name, it required its own dedicated PrepareRename
RPC in the protocol.)
"Add struct tags", without questions
The language server must deduce the user’s intent from context alone, namely the cursor position and the enclosing syntax tree. When that context is not enough, the server is forced into one of two choices: apply a one-size-fits-all default, or refrain from offering the transformation altogether. The limitation lies not in gopls’s ability to compute the necessary edits, but in the LSP’s lack of a mechanism for language servers to ask the user for input.
For many transformations, this parameterless model works well. But many others
need additional input from the user: which struct tags to add and in what
naming convention, which interface to implement, which file to move a
declaration to. Without a way to ask, gopls could only guess—“Add struct
tags”, for example, always produced json tags in camelCase—or not offer the
refactoring at all. With interactive refactoring, language servers can now
easily provide new operations with one or more parameters.
"Add struct tags", with questions
While some language extensions have worked around this limitation by embedding custom, language-specific dialog logic directly into individual editors, doing so leaves behind other editors (such as Neovim and Emacs) that stick strictly to the LSP with minimal custom client code. More importantly, editor-specific workarounds go against the fundamental purpose of the LSP: to decouple language-specific features from editors so that each server improvement benefits every client.
Our LSP interactive refactoring proposal (microsoft/language-server-protocol#1164) solves this by establishing a standard mechanism for language servers to collect user input before computing text edits. For Go developers, this unlocks a suite of long-awaited refactorings, including:
among many others.
Adding and removing struct tags (gopls v0.23.0)
When configuring serialization formats, developers frequently need custom tags
and casing conventions. Previously, running “Add struct tags” unconditionally
injected json tags using camelCase formatting.
With interactive refactoring:
json, allowing you to customize
any combination of tags (such as json,yaml,db).camelCase, snake_case, or PascalCase).Interactive "Add struct tags" in VS Code
Here is the before-and-after transformation:
type UserProfile struct {
ID int64
FirstName string
LastName string
EmailAddr string
}
type UserProfile struct {
ID int64 `json:"id" db:"id"`
FirstName string `json:"first_name" db:"first_name"`
LastName string `json:"last_name" db:"last_name"`
EmailAddr string `json:"email_addr" db:"email_addr"`
}
Similarly, “Remove struct tags” now asks you for the specific tag keys
you wish to delete (for example, removing only db tags while preserving the json
tag), rather than wiping out all struct tags indiscriminately.
Interactive "Remove struct tags" in VS Code
Implementing interfaces with dynamic symbol search
When creating a type to satisfy an interface, developers previously had to copy
the interface declaration and manually turn it into a set of methods on the
receiver type (or rely on external tools like impl by manually typing the
receiver and qualifying the name of the interface).
Since gopls v0.23.0, with interactive refactoring:
maphash.Hasher).Foo).Interactive "Add methods to implement an interface..." in VS Code
type Foo struct{}
type MyHasher struct{}
type Foo struct{}
type MyHasher struct{}
// Equal implements [maphash.Hasher].
func (m *MyHasher) Equal(x Foo, y Foo) bool {
panic("unimplemented")
}
// Hash implements [maphash.Hasher].
func (m *MyHasher) Hash(*maphash.Hash, Foo) {
panic("unimplemented")
}
Moving declarations across files and packages (preview)
Moving declarations manually by cutting and pasting can easily break references and requires tedious import cleanup across multiple files. While scheduled for an upcoming gopls release, our working implementation already demonstrates how interactive file selection streamlines cross-file and cross-package moves:
.go file or
specify a new file path.Interactive "Move declaration" in VS Code
In the example below, we move the type Foo from package from to package
to. Its method Print and the unexported constant bar, which only Foo
uses, move along with it, while the unrelated function Baz stays behind:
// from/from.go
package from
import (
"fmt"
"strings"
)
const bar = "bar"
// Doc comment for Foo
type Foo struct{}
func (f Foo) Print() {
fmt.Println(bar)
}
func Baz() string {
return strings.ToUpper("baz")
}
// from/from.go
package from
import "strings"
func Baz() string {
return strings.ToUpper("baz")
}
// to/to.go
package to
import "fmt"
const bar = "bar"
// Doc comment for Foo
type Foo struct{}
func (f Foo) Print() {
fmt.Println(bar)
}
Slaying the dragon (gopls v0.24.0)
As a demonstration of multi-step interactive dialogs and a self-contained test
case for client editor implementations, gopls v0.24.0 offers a “Slay the
dragon” code action when you place your cursor on a package-level variable
named dragonSlayer (for example, var dragonSlayer string). Happy hunting!
Interactive refactoring establishes a foundational capability in gopls, opening up several exciting directions on our roadmap:
Interactive refactoring is available in gopls v0.24.0
and vscode-go v0.56.0
(with proof-of-concept client integrations also explored for
Neovim and
Emacs).
We would love to hear your thoughts as we continue refining interactive code actions in gopls:
(Special thanks to Alan Donovan, Alex Putman, Brian Wilkerson, Danny Tuppeny, Hana Kim, Madeline Kalil, Peter Weinberger, and Robert Findley for their insights, discussions, feedback, and collaboration on this work.)
Happy refactoring!