internal/lsp/command: add an interface for workspace/executeCommand

This CL lays the groundwork for future refactoring, by defining a formal
(Go) interface for the set of commands provided by gopls in the
workspace/executeCommand RPC. It then creates some boilerplate bindings
via code generation.

The intent is to, first of all, clean up our current usage of commands.
Currently the 'specification' of a command is really split across
internal/lsp/command.go, internal/lsp/source/command.go, and
internal/lsp/source/code_lens.go. Changing a command signature might
require altering all three of those files, and it's easy to get wrong.

But also, we'd like to eventually be able to tell plugin authors that
they can call our commands in an ad-hoc manner (meaning with arguments
that they assign, rather than extract from a code lens). In order to do
that, we need to be able to generate documentation for the command
signature, and should also stop using positional arguments.  This CL
aims to solve that as well, by providing a commandmeta package that can
be used for document generation.

For golang/go#40438

Change-Id: I0d29de044e107d6e7b267f340879a5282f0b4944
Reviewed-on: https://go-review.googlesource.com/c/tools/+/289489
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/internal/lsp/command/command_gen.go b/internal/lsp/command/command_gen.go
new file mode 100644
index 0000000..d740afa
--- /dev/null
+++ b/internal/lsp/command/command_gen.go
@@ -0,0 +1,266 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package command
+
+// Code generated by generate.go. DO NOT EDIT.
+
+import (
+	"fmt"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+)
+
+func Dispatch(params *protocol.ExecuteCommandParams, s Interface) (interface{}, error) {
+	switch params.Command {
+	case "gopls.add_dependency":
+		var a0 DependencyArgs
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		err := s.AddDependency(a0)
+		return nil, err
+	case "gopls.check_upgrades":
+		var a0 CheckUpgradesArgs
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		err := s.CheckUpgrades(a0)
+		return nil, err
+	case "gopls.generate":
+		var a0 GenerateArgs
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		err := s.Generate(a0)
+		return nil, err
+	case "gopls.generate_gopls_mod":
+		var a0 URIArg
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		err := s.GenerateGoplsMod(a0)
+		return nil, err
+	case "gopls.go_get_package":
+		var a0 GoGetPackageArgs
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		err := s.GoGetPackage(a0)
+		return nil, err
+	case "gopls.regenerate_cgo":
+		var a0 URIArg
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		err := s.RegenerateCgo(a0)
+		return nil, err
+	case "gopls.remove_dependency":
+		var a0 RemoveDependencyArgs
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		err := s.RemoveDependency(a0)
+		return nil, err
+	case "gopls.run_tests":
+		var a0 RunTestsArgs
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		err := s.RunTests(a0)
+		return nil, err
+	case "gopls.tidy":
+		var a0 URIArg
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		err := s.Tidy(a0)
+		return nil, err
+	case "gopls.toggle_details":
+		var a0 URIArg
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		err := s.ToggleDetails(a0)
+		return nil, err
+	case "gopls.update_go_sum":
+		var a0 URIArg
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		err := s.UpdateGoSum(a0)
+		return nil, err
+	case "gopls.upgrade_dependency":
+		var a0 DependencyArgs
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		err := s.UpgradeDependency(a0)
+		return nil, err
+	case "gopls.vendor":
+		var a0 URIArg
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		err := s.Vendor(a0)
+		return nil, err
+	}
+	return nil, fmt.Errorf("unsupported command %q", params.Command)
+}
+
+func NewAddDependencyCommand(title string, a0 DependencyArgs) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   "gopls.add_dependency",
+		Arguments: args,
+	}, nil
+}
+
+func NewCheckUpgradesCommand(title string, a0 CheckUpgradesArgs) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   "gopls.check_upgrades",
+		Arguments: args,
+	}, nil
+}
+
+func NewGenerateCommand(title string, a0 GenerateArgs) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   "gopls.generate",
+		Arguments: args,
+	}, nil
+}
+
+func NewGenerateGoplsModCommand(title string, a0 URIArg) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   "gopls.generate_gopls_mod",
+		Arguments: args,
+	}, nil
+}
+
+func NewGoGetPackageCommand(title string, a0 GoGetPackageArgs) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   "gopls.go_get_package",
+		Arguments: args,
+	}, nil
+}
+
+func NewRegenerateCgoCommand(title string, a0 URIArg) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   "gopls.regenerate_cgo",
+		Arguments: args,
+	}, nil
+}
+
+func NewRemoveDependencyCommand(title string, a0 RemoveDependencyArgs) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   "gopls.remove_dependency",
+		Arguments: args,
+	}, nil
+}
+
+func NewRunTestsCommand(title string, a0 RunTestsArgs) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   "gopls.run_tests",
+		Arguments: args,
+	}, nil
+}
+
+func NewTidyCommand(title string, a0 URIArg) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   "gopls.tidy",
+		Arguments: args,
+	}, nil
+}
+
+func NewToggleDetailsCommand(title string, a0 URIArg) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   "gopls.toggle_details",
+		Arguments: args,
+	}, nil
+}
+
+func NewUpdateGoSumCommand(title string, a0 URIArg) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   "gopls.update_go_sum",
+		Arguments: args,
+	}, nil
+}
+
+func NewUpgradeDependencyCommand(title string, a0 DependencyArgs) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   "gopls.upgrade_dependency",
+		Arguments: args,
+	}, nil
+}
+
+func NewVendorCommand(title string, a0 URIArg) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   "gopls.vendor",
+		Arguments: args,
+	}, nil
+}
diff --git a/internal/lsp/command/commandmeta/meta.go b/internal/lsp/command/commandmeta/meta.go
new file mode 100644
index 0000000..0859eb5
--- /dev/null
+++ b/internal/lsp/command/commandmeta/meta.go
@@ -0,0 +1,219 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package commandmeta provides metadata about LSP commands, by analyzing the
+// command.Interface type.
+package commandmeta
+
+import (
+	"fmt"
+	"go/ast"
+	"go/token"
+	"go/types"
+	"reflect"
+	"strings"
+	"unicode"
+
+	"golang.org/x/tools/go/ast/astutil"
+	"golang.org/x/tools/go/packages"
+	"golang.org/x/tools/internal/lsp/command"
+)
+
+type Command struct {
+	MethodName string
+	Name       string
+	// TODO(rFindley): I think Title can actually be eliminated. In all cases
+	// where we use it, there is probably a more appropriate contextual title.
+	Title  string
+	Doc    string
+	Args   []*Field
+	Result types.Type
+}
+
+type Field struct {
+	Name    string
+	Doc     string
+	JSONTag string
+	Type    types.Type
+	// In some circumstances, we may want to recursively load additional field
+	// descriptors for fields of struct types, documenting their internals.
+	Fields []*Field
+}
+
+func Load() (*packages.Package, []*Command, error) {
+	pkgs, err := packages.Load(
+		&packages.Config{
+			Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedDeps,
+		},
+		"golang.org/x/tools/internal/lsp/command",
+	)
+	if err != nil {
+		return nil, nil, err
+	}
+	pkg := pkgs[0]
+
+	// For a bit of type safety, use reflection to get the interface name within
+	// the package scope.
+	it := reflect.TypeOf((*command.Interface)(nil)).Elem()
+	obj := pkg.Types.Scope().Lookup(it.Name()).Type().Underlying().(*types.Interface)
+
+	// Load command metadata corresponding to each interface method.
+	var commands []*Command
+	loader := fieldLoader{make(map[types.Object]*Field)}
+	for i := 0; i < obj.NumMethods(); i++ {
+		m := obj.Method(i)
+		c, err := loader.loadMethod(pkg, m)
+		if err != nil {
+			return nil, nil, fmt.Errorf("loading %s: %v", m.Name(), err)
+		}
+		commands = append(commands, c)
+	}
+	return pkg, commands, nil
+}
+
+// fieldLoader loads field information, memoizing results to prevent infinite
+// recursion.
+type fieldLoader struct {
+	loaded map[types.Object]*Field
+}
+
+var universeError = types.Universe.Lookup("error").Type()
+
+func (l *fieldLoader) loadMethod(pkg *packages.Package, m *types.Func) (*Command, error) {
+	node, err := findField(pkg, m.Pos())
+	if err != nil {
+		return nil, err
+	}
+	title, doc := splitDoc(node.Doc.Text())
+	c := &Command{
+		MethodName: m.Name(),
+		Name:       lspName(m.Name()),
+		Doc:        doc,
+		Title:      title,
+	}
+	sig := m.Type().Underlying().(*types.Signature)
+	rlen := sig.Results().Len()
+	if rlen > 2 || rlen == 0 {
+		return nil, fmt.Errorf("must have 1 or 2 returns, got %d", rlen)
+	}
+	finalResult := sig.Results().At(rlen - 1)
+	if !types.Identical(finalResult.Type(), universeError) {
+		return nil, fmt.Errorf("final return must be error")
+	}
+	if rlen == 2 {
+		c.Result = sig.Results().At(0).Type()
+	}
+	ftype := node.Type.(*ast.FuncType)
+	if sig.Params().Len() != ftype.Params.NumFields() {
+		panic("bug: mismatching method params")
+	}
+	for i, p := range ftype.Params.List {
+		pt := sig.Params().At(i)
+		fld, err := l.loadField(pkg, p, pt, "")
+		if err != nil {
+			return nil, err
+		}
+		c.Args = append(c.Args, fld)
+	}
+	return c, nil
+}
+
+func (l *fieldLoader) loadField(pkg *packages.Package, node *ast.Field, obj *types.Var, tag string) (*Field, error) {
+	if existing, ok := l.loaded[obj]; ok {
+		return existing, nil
+	}
+	fld := &Field{
+		Name:    obj.Name(),
+		Doc:     strings.TrimSpace(node.Doc.Text()),
+		Type:    obj.Type(),
+		JSONTag: reflect.StructTag(tag).Get("json"),
+	}
+	under := fld.Type.Underlying()
+	if p, ok := under.(*types.Pointer); ok {
+		under = p.Elem()
+	}
+	if s, ok := under.(*types.Struct); ok {
+		for i := 0; i < s.NumFields(); i++ {
+			obj2 := s.Field(i)
+			pkg2 := pkg
+			if obj2.Pkg() != pkg2.Types {
+				pkg2 = pkg.Imports[obj2.Pkg().Path()]
+			}
+			node2, err := findField(pkg2, obj2.Pos())
+			if err != nil {
+				return nil, err
+			}
+			tag := s.Tag(i)
+			structField, err := l.loadField(pkg2, node2, obj2, tag)
+			if err != nil {
+				return nil, err
+			}
+			fld.Fields = append(fld.Fields, structField)
+		}
+	}
+	return fld, nil
+}
+
+// splitDoc parses a command doc string to separate the title from normal
+// documentation.
+//
+// The doc comment should be of the form: "MethodName: Title\nDocumentation"
+func splitDoc(text string) (title, doc string) {
+	docParts := strings.SplitN(doc, "\n", 2)
+	titleParts := strings.SplitN(docParts[0], ":", 2)
+	if len(titleParts) > 1 {
+		title = strings.TrimSpace(titleParts[1])
+	}
+	if len(docParts) > 1 {
+		doc = strings.TrimSpace(docParts[1])
+	}
+	return title, doc
+}
+
+// lspName returns the normalized command name to use in the LSP.
+func lspName(methodName string) string {
+	words := splitCamel(methodName)
+	for i := range words {
+		words[i] = strings.ToLower(words[i])
+	}
+	return "gopls." + strings.Join(words, "_")
+}
+
+// splitCamel splits s into words, according to camel-case word boundaries.
+func splitCamel(s string) []string {
+	var words []string
+	for len(s) > 0 {
+		last := strings.LastIndexFunc(s, unicode.IsUpper)
+		if last < 0 {
+			last = 0
+		}
+		words = append(words, s[last:])
+		s = s[:last]
+	}
+	for i := 0; i < len(words)/2; i++ {
+		j := len(words) - i - 1
+		words[i], words[j] = words[j], words[i]
+	}
+	return words
+}
+
+// findField finds the struct field or interface method positioned at pos,
+// within the AST.
+func findField(pkg *packages.Package, pos token.Pos) (*ast.Field, error) {
+	fset := pkg.Fset
+	var file *ast.File
+	for _, f := range pkg.Syntax {
+		if fset.Position(f.Pos()).Filename == fset.Position(pos).Filename {
+			file = f
+			break
+		}
+	}
+	if file == nil {
+		return nil, fmt.Errorf("no file for pos %v", pos)
+	}
+	path, _ := astutil.PathEnclosingInterval(file, pos, pos)
+	// This is fragile, but in the cases we care about, the field will be in
+	// path[1].
+	return path[1].(*ast.Field), nil
+}
diff --git a/internal/lsp/command/generate.go b/internal/lsp/command/generate.go
new file mode 100644
index 0000000..49d72b5
--- /dev/null
+++ b/internal/lsp/command/generate.go
@@ -0,0 +1,24 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build ignore
+
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+
+	"golang.org/x/tools/internal/lsp/command/generate"
+)
+
+func main() {
+	content, err := generate.Generate()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "%v\n", err)
+		os.Exit(1)
+	}
+	ioutil.WriteFile("command_gen.go", content, 0644)
+}
diff --git a/internal/lsp/command/generate/generate.go b/internal/lsp/command/generate/generate.go
new file mode 100644
index 0000000..d206e22
--- /dev/null
+++ b/internal/lsp/command/generate/generate.go
@@ -0,0 +1,136 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package generate is used to generate command bindings from the gopls command
+// interface.
+package generate
+
+import (
+	"bytes"
+	"fmt"
+	"go/types"
+	"text/template"
+
+	"golang.org/x/tools/internal/imports"
+	"golang.org/x/tools/internal/lsp/command/commandmeta"
+)
+
+const src = `// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package command
+
+// Code generated by generate.go. DO NOT EDIT.
+
+import (
+	{{range $k, $v := .Imports -}}
+	"{{$k}}"
+	{{end}}
+)
+
+func Dispatch(params *protocol.ExecuteCommandParams, s Interface) (interface{}, error) {
+	switch params.Command {
+	{{- range .Commands}}
+	case "{{.Name}}":
+		{{- if .Args -}}
+			{{- range $i, $v := .Args}}
+		var a{{$i}} {{typeString $v.Type}}
+			{{- end}}
+		if err := UnmarshalArgs(params.Arguments{{range $i, $v := .Args}}, &a{{$i}}{{end}}); err != nil {
+			return nil, err
+		}
+		{{end -}}
+		{{- if .Result -}}res, {{end}}err := s.{{.MethodName}}({{block "callargs" .}}{{range $i, $v := .Args}}{{if $i}}, {{end}}a{{$i}}{{end}}{{end}})
+		return {{if .Result}}res{{else}}nil{{end}}, err
+	{{- end}}
+	}
+	return nil, fmt.Errorf("unsupported command %q", params.Command)
+}
+{{- range .Commands}}
+
+func New{{.MethodName}}Command(title string, {{range $i, $v := .Args}}{{if $i}}, {{end}}a{{$i}} {{typeString $v.Type}}{{end}}) (protocol.Command, error) {
+	args, err := MarshalArgs({{template "callargs" .}})
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title: title,
+		Command: "{{.Name}}",
+		Arguments: args,
+	}, nil
+}
+{{end}}
+`
+
+type data struct {
+	Imports  map[string]bool
+	Commands []*commandmeta.Command
+}
+
+func Generate() ([]byte, error) {
+	pkg, cmds, err := commandmeta.Load()
+	if err != nil {
+		return nil, err
+	}
+	qf := func(p *types.Package) string {
+		if p == pkg.Types {
+			return ""
+		}
+		return p.Name()
+	}
+	tmpl, err := template.New("").Funcs(template.FuncMap{
+		"typeString": func(t types.Type) string {
+			return types.TypeString(t, qf)
+		},
+	}).Parse(src)
+	if err != nil {
+		return nil, err
+	}
+	d := data{
+		Commands: cmds,
+		Imports: map[string]bool{
+			"fmt": true,
+			"golang.org/x/tools/internal/lsp/protocol": true,
+		},
+	}
+	const thispkg = "golang.org/x/tools/internal/lsp/command"
+	for _, c := range d.Commands {
+		for _, arg := range c.Args {
+			pth := pkgPath(arg.Type)
+			if pth != "" && pth != thispkg {
+				d.Imports[pth] = true
+			}
+		}
+		pth := pkgPath(c.Result)
+		if pth != "" && pth != thispkg {
+			d.Imports[pth] = true
+		}
+	}
+
+	var buf bytes.Buffer
+	if err := tmpl.Execute(&buf, d); err != nil {
+		return nil, fmt.Errorf("executing: %v", err)
+	}
+
+	opts := &imports.Options{
+		AllErrors:  true,
+		FormatOnly: true,
+		Comments:   true,
+	}
+	content, err := imports.Process("", buf.Bytes(), opts)
+	if err != nil {
+		return nil, fmt.Errorf("goimports: %v", err)
+	}
+	return content, nil
+}
+
+func pkgPath(t types.Type) string {
+	if n, ok := t.(*types.Named); ok {
+		if pkg := n.Obj().Pkg(); pkg != nil {
+			return pkg.Path()
+		}
+	}
+	return ""
+}
diff --git a/internal/lsp/command/interface.go b/internal/lsp/command/interface.go
new file mode 100644
index 0000000..7ffcd85
--- /dev/null
+++ b/internal/lsp/command/interface.go
@@ -0,0 +1,143 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package command defines the interface provided by gopls for the
+// workspace/executeCommand LSP request.
+//
+// This interface is fully specified by the Interface type, provided it
+// conforms to the restrictions outlined in its doc string.
+//
+// Bindings for server-side command dispatch and client-side serialization are
+// also provided by this package, via code generation.
+package command
+
+//go:generate go run generate.go
+
+import "golang.org/x/tools/internal/lsp/protocol"
+
+// Interface defines the interface gopls exposes for the
+// workspace/executeCommand request.
+//
+// This interface is used to generate marshaling/unmarshaling code, dispatch,
+// and documentation, and so has some additional restrictions:
+//  1. All method arguments must be JSON serializable.
+//  2. Methods must return either error or (T, error), where T is a
+//     JSON serializable type.
+//  3. The first line of the doc string is special. Everything after the colon
+//     is considered the command 'Title'.
+//     TODO(rFindley): reconsider this -- Title may be unnecessary.
+type Interface interface {
+	// RunTests: Run test(s)
+	//
+	// Runs `go test` for a specific set of test or benchmark functions.
+	RunTests(RunTestsArgs) error
+
+	// Generate: Run go generate
+	//
+	// Runs `go generate` for a given directory.
+	Generate(GenerateArgs) error
+
+	// RegenerateCgo: Regenerate cgo
+	//
+	// Regenerates cgo definitions.
+	RegenerateCgo(URIArg) error
+
+	// Tidy: Run go mod tidy
+	//
+	// Runs `go mod tidy` for a module.
+	Tidy(URIArg) error
+
+	// Vendor: Run go mod vendor
+	//
+	// Runs `go mod vendor` for a module.
+	Vendor(URIArg) error
+
+	// UpdateGoSum: Update go.sum
+	//
+	// Updates the go.sum file for a module.
+	UpdateGoSum(URIArg) error
+
+	// CheckUpgrades: Check for upgrades
+	//
+	// Checks for module upgrades.
+	CheckUpgrades(CheckUpgradesArgs) error
+
+	// AddDependency: Add dependency
+	//
+	// Adds a dependency to the go.mod file for a module.
+	AddDependency(DependencyArgs) error
+
+	// UpgradeDependency: Upgrade dependency
+	//
+	// Upgrades a dependency in the go.mod file for a module.
+	UpgradeDependency(DependencyArgs) error
+
+	// RemoveDependency: Remove dependency
+	//
+	// Removes a dependency from the go.mod file of a module.
+	RemoveDependency(RemoveDependencyArgs) error
+
+	// GoGetPackage: go get package
+	//
+	// Runs `go get` to fetch a package.
+	GoGetPackage(GoGetPackageArgs) error
+
+	// ToggleDetails: Toggle gc_details
+	//
+	// Toggle the calculation of gc annotations.
+	ToggleDetails(URIArg) error
+
+	// GenerateGoplsMod: Generate gopls.mod
+	//
+	// (Re)generate the gopls.mod file for a workspace.
+	GenerateGoplsMod(URIArg) error
+}
+
+type RunTestsArgs struct {
+	// URI is the test file containing the tests to run.
+	URI protocol.DocumentURI
+
+	// Tests holds specific test names to run, e.g. TestFoo.
+	Tests []string
+
+	// Benchmarks holds specific benchmarks to run, e.g. BenchmarkFoo.
+	Benchmarks []string
+}
+
+type GenerateArgs struct {
+	// URI is any file within the directory to generate. Usually this is the file
+	// containing the '//go:generate' directive.
+	URI protocol.DocumentURI
+
+	// Recursive controls whether to generate recursively (go generate ./...)
+	Recursive bool
+}
+
+// TODO(rFindley): document the rest of these once the docgen is fleshed out.
+
+type URIArg struct {
+	URI protocol.DocumentURI
+}
+
+type CheckUpgradesArgs struct {
+	URI     protocol.DocumentURI
+	Modules []string
+}
+
+type DependencyArgs struct {
+	URI        protocol.DocumentURI
+	GoCmdArgs  []string
+	AddRequire bool
+}
+
+type RemoveDependencyArgs struct {
+	URI            protocol.DocumentURI
+	ModulePath     string
+	OnlyDiagnostic bool
+}
+
+type GoGetPackageArgs struct {
+	URI protocol.DocumentURI
+	Pkg string
+}
diff --git a/internal/lsp/command/interface_test.go b/internal/lsp/command/interface_test.go
new file mode 100644
index 0000000..d58545e
--- /dev/null
+++ b/internal/lsp/command/interface_test.go
@@ -0,0 +1,31 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package command_test
+
+import (
+	"bytes"
+	"io/ioutil"
+	"testing"
+
+	"golang.org/x/tools/internal/lsp/command/generate"
+	"golang.org/x/tools/internal/testenv"
+)
+
+func TestGenerated(t *testing.T) {
+	testenv.NeedsGoBuild(t) // This is a lie. We actually need the source code.
+
+	onDisk, err := ioutil.ReadFile("command_gen.go")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	generated, err := generate.Generate()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !bytes.Equal(onDisk, generated) {
+		t.Error("command_gen.go is stale -- regenerate")
+	}
+}
diff --git a/internal/lsp/command/util.go b/internal/lsp/command/util.go
new file mode 100644
index 0000000..c81aaf6
--- /dev/null
+++ b/internal/lsp/command/util.go
@@ -0,0 +1,54 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package command
+
+import (
+	"encoding/json"
+	"fmt"
+)
+
+// MarshalArgs encodes the given arguments to json.RawMessages. This function
+// is used to construct arguments to a protocol.Command.
+//
+// Example usage:
+//
+//   jsonArgs, err := EncodeArgs(1, "hello", true, StructuredArg{42, 12.6})
+//
+func MarshalArgs(args ...interface{}) ([]json.RawMessage, error) {
+	var out []json.RawMessage
+	for _, arg := range args {
+		argJSON, err := json.Marshal(arg)
+		if err != nil {
+			return nil, err
+		}
+		out = append(out, argJSON)
+	}
+	return out, nil
+}
+
+// UnmarshalArgs decodes the given json.RawMessages to the variables provided
+// by args. Each element of args should be a pointer.
+//
+// Example usage:
+//
+//   var (
+//       num int
+//       str string
+//       bul bool
+//       structured StructuredArg
+//   )
+//   err := UnmarshalArgs(args, &num, &str, &bul, &structured)
+//
+func UnmarshalArgs(jsonArgs []json.RawMessage, args ...interface{}) error {
+	if len(args) != len(jsonArgs) {
+		return fmt.Errorf("DecodeArgs: expected %d input arguments, got %d JSON arguments", len(args), len(jsonArgs))
+	}
+	for i, arg := range args {
+		if err := json.Unmarshal(jsonArgs[i], arg); err != nil {
+			return err
+		}
+	}
+	return nil
+}