internal/lsp: switch to the new command API

Fully switch to the new generated command API, and remove the old
dynamic command configuration.

This involved several steps:
 + Switch the command dispatch in internal/lsp/command.go to go through
   the command package. This means that all commands must now use the new
   signature.
 + Update commandHandler to use the new command signatures.
 + Fix some errors discovered in the command interface now that we're
   actually using it.
 + Regenerate bindings.
 + Update all code lens and suggested fixes to new the new command
   constructors.
 + Generate values in the command package to hold command names and the
   full set of commands, so that they may be referenced by name.
 + Update any references to command names to use the command package.
 + Delete command metadata from the source package. Rename command.go to
   fix.go.
 + Update lsp tests to execute commands directly rather than use an
   internal API. This involved a bit of hackery to collect the edits.
 + Update document generation to use command metadata. Documenting the
   arguments is left to a later CL.
 + Various small fixes related to the above.

This change is intended to be invisible to users. We have changed the
command signatures, but have not (previously) committed to backwards
compatibility for commands. Notably, the gopls.test and gopls.gc_details
signatures are preserved, as these are the two cases where we are aware
of LSP clients calling them directly, not from a code lens or
diagnostic.

For golang/go#40438

Change-Id: Ie1b92c95d6ce7e2fc25fc029d1f85b942f40e851
Reviewed-on: https://go-review.googlesource.com/c/tools/+/290111
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: Heschi Kreinick <heschi@google.com>
diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md
index ee50882..da83371 100644
--- a/gopls/doc/commands.md
+++ b/gopls/doc/commands.md
@@ -6,105 +6,81 @@
 ### **Add dependency**
 Identifier: `gopls.add_dependency`
 
-add_dependency adds a dependency.
+Adds a dependency to the go.mod file for a module.
 
+### **Apply a fix**
+Identifier: `gopls.apply_fix`
+
+Applies a fix to a region of source code.
 
 ### **Check for upgrades**
 Identifier: `gopls.check_upgrades`
 
-check_upgrades checks for module upgrades.
-
-
-### **Extract to function**
-Identifier: `gopls.extract_function`
-
-extract_function extracts statements to a function.
-
-
-### **Extract to variable**
-Identifier: `gopls.extract_variable`
-
-extract_variable extracts an expression to a variable.
-
-
-### **Fill struct**
-Identifier: `gopls.fill_struct`
-
-fill_struct is a gopls command to fill a struct with default
-values.
-
+Checks for module upgrades.
 
 ### **Toggle gc_details**
 Identifier: `gopls.gc_details`
 
-gc_details controls calculation of gc annotations.
-
+Toggle the calculation of gc annotations.
 
 ### **Run go generate**
 Identifier: `gopls.generate`
 
-generate runs `go generate` for a given directory.
-
+Runs `go generate` for a given directory.
 
 ### **Generate gopls.mod**
 Identifier: `gopls.generate_gopls_mod`
 
-generate_gopls_mod (re)generates the gopls.mod file.
-
+(Re)generate the gopls.mod file for a workspace.
 
 ### **go get package**
 Identifier: `gopls.go_get_package`
 
-go_get_package runs `go get` to fetch a package.
-
+Runs `go get` to fetch a package.
 
 ### **Regenerate cgo**
 Identifier: `gopls.regenerate_cgo`
 
-regenerate_cgo regenerates cgo definitions.
-
+Regenerates cgo definitions.
 
 ### **Remove dependency**
 Identifier: `gopls.remove_dependency`
 
-remove_dependency removes a dependency.
-
+Removes a dependency from the go.mod file of a module.
 
 ### **Run test(s)**
+Identifier: `gopls.run_tests`
+
+Runs `go test` for a specific set of test or benchmark functions.
+
+### **Run test(s) (legacy)**
 Identifier: `gopls.test`
 
-test runs `go test` for a specific test function.
-
+Runs `go test` for a specific set of test or benchmark functions.
 
 ### **Run go mod tidy**
 Identifier: `gopls.tidy`
 
-tidy runs `go mod tidy` for a module.
+Runs `go mod tidy` for a module.
 
+### **Toggle gc_details**
+Identifier: `gopls.toggle_gc_details`
 
-### **Undeclared name**
-Identifier: `gopls.undeclared_name`
-
-undeclared_name adds a variable declaration for an undeclared
-name.
-
+Toggle the calculation of gc annotations.
 
 ### **Update go.sum**
 Identifier: `gopls.update_go_sum`
 
-update_go_sum updates the go.sum file for a module.
-
+Updates the go.sum file for a module.
 
 ### **Upgrade dependency**
 Identifier: `gopls.upgrade_dependency`
 
-upgrade_dependency upgrades a dependency.
-
+Upgrades a dependency in the go.mod file for a module.
 
 ### **Run go mod vendor**
 Identifier: `gopls.vendor`
 
-vendor runs `go mod vendor` for a module.
-
+Runs `go mod vendor` for a module.
 
 <!-- END Commands: DO NOT MANUALLY EDIT THIS SECTION -->
diff --git a/gopls/doc/generate.go b/gopls/doc/generate.go
index 1eff8b9..c6ba55b 100644
--- a/gopls/doc/generate.go
+++ b/gopls/doc/generate.go
@@ -29,6 +29,8 @@
 	"github.com/sanity-io/litter"
 	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/go/packages"
+	"golang.org/x/tools/internal/lsp/command"
+	"golang.org/x/tools/internal/lsp/command/commandmeta"
 	"golang.org/x/tools/internal/lsp/mod"
 	"golang.org/x/tools/internal/lsp/source"
 )
@@ -87,7 +89,7 @@
 
 	// Transform the internal command name to the external command name.
 	for _, c := range api.Commands {
-		c.Command = source.CommandPrefix + c.Command
+		c.Command = command.ID(c.Command)
 	}
 	for _, m := range []map[string]source.Analyzer{
 		defaults.DefaultAnalyzers,
@@ -368,90 +370,40 @@
 }
 
 func loadCommands(pkg *packages.Package) ([]*source.CommandJSON, error) {
-	// The code that defines commands is much more complicated than the
-	// code that defines options, so reading comments for the Doc is very
-	// fragile. If this causes problems, we should switch to a dynamic
-	// approach and put the doc in the Commands struct rather than reading
-	// from the source code.
-
-	// Find the Commands slice.
-	typesSlice := pkg.Types.Scope().Lookup("Commands")
-	f, err := fileForPos(pkg, typesSlice.Pos())
-	if err != nil {
-		return nil, err
-	}
-	path, _ := astutil.PathEnclosingInterval(f, typesSlice.Pos(), typesSlice.Pos())
-	vspec := path[1].(*ast.ValueSpec)
-	var astSlice *ast.CompositeLit
-	for i, name := range vspec.Names {
-		if name.Name == "Commands" {
-			astSlice = vspec.Values[i].(*ast.CompositeLit)
-		}
-	}
 
 	var commands []*source.CommandJSON
 
+	_, cmds, err := commandmeta.Load()
+	if err != nil {
+		return nil, err
+	}
 	// Parse the objects it contains.
-	for _, elt := range astSlice.Elts {
-		// Find the composite literal of the Command.
-		typesCommand := pkg.TypesInfo.ObjectOf(elt.(*ast.Ident))
-		path, _ := astutil.PathEnclosingInterval(f, typesCommand.Pos(), typesCommand.Pos())
-		vspec := path[1].(*ast.ValueSpec)
-
-		var astCommand ast.Expr
-		for i, name := range vspec.Names {
-			if name.Name == typesCommand.Name() {
-				astCommand = vspec.Values[i]
-			}
-		}
-
-		// Read the Name and Title fields of the literal.
-		var name, title string
-		ast.Inspect(astCommand, func(n ast.Node) bool {
-			kv, ok := n.(*ast.KeyValueExpr)
-			if ok {
-				k := kv.Key.(*ast.Ident).Name
-				switch k {
-				case "Name":
-					name = strings.Trim(kv.Value.(*ast.BasicLit).Value, `"`)
-				case "Title":
-					title = strings.Trim(kv.Value.(*ast.BasicLit).Value, `"`)
-				}
-			}
-			return true
-		})
-
-		if title == "" {
-			title = name
-		}
-
-		// Conventionally, the doc starts with the name of the variable.
-		// Replace it with the name of the command.
-		doc := vspec.Doc.Text()
-		doc = strings.Replace(doc, typesCommand.Name(), name, 1)
-
+	for _, cmd := range cmds {
 		commands = append(commands, &source.CommandJSON{
-			Command: name,
-			Title:   title,
-			Doc:     doc,
+			Command: cmd.Name,
+			Title:   cmd.Title,
+			Doc:     cmd.Doc,
 		})
 	}
 	return commands, nil
 }
 
 func loadLenses(commands []*source.CommandJSON) []*source.LensJSON {
-	lensNames := map[string]struct{}{}
+	all := map[command.Command]struct{}{}
 	for k := range source.LensFuncs() {
-		lensNames[k] = struct{}{}
+		all[k] = struct{}{}
 	}
 	for k := range mod.LensFuncs() {
-		lensNames[k] = struct{}{}
+		if _, ok := all[k]; ok {
+			panic(fmt.Sprintf("duplicate lens %q", string(k)))
+		}
+		all[k] = struct{}{}
 	}
 
 	var lenses []*source.LensJSON
 
 	for _, cmd := range commands {
-		if _, ok := lensNames[cmd.Command]; ok {
+		if _, ok := all[command.Command(cmd.Command)]; ok {
 			lenses = append(lenses, &source.LensJSON{
 				Lens:  cmd.Command,
 				Title: cmd.Title,
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md
index df7258f..f48dc3f 100644
--- a/gopls/doc/settings.md
+++ b/gopls/doc/settings.md
@@ -388,42 +388,35 @@
 
 Identifier: `gc_details`
 
-gc_details controls calculation of gc annotations.
-
+Toggle the calculation of gc annotations.
 ### **Run go generate**
 
 Identifier: `generate`
 
-generate runs `go generate` for a given directory.
-
+Runs `go generate` for a given directory.
 ### **Regenerate cgo**
 
 Identifier: `regenerate_cgo`
 
-regenerate_cgo regenerates cgo definitions.
-
-### **Run test(s)**
+Regenerates cgo definitions.
+### **Run test(s) (legacy)**
 
 Identifier: `test`
 
-test runs `go test` for a specific test function.
-
+Runs `go test` for a specific set of test or benchmark functions.
 ### **Run go mod tidy**
 
 Identifier: `tidy`
 
-tidy runs `go mod tidy` for a module.
-
+Runs `go mod tidy` for a module.
 ### **Upgrade dependency**
 
 Identifier: `upgrade_dependency`
 
-upgrade_dependency upgrades a dependency.
-
+Upgrades a dependency in the go.mod file for a module.
 ### **Run go mod vendor**
 
 Identifier: `vendor`
 
-vendor runs `go mod vendor` for a module.
-
+Runs `go mod vendor` for a module.
 <!-- END Lenses: DO NOT MANUALLY EDIT THIS SECTION -->
diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go
index 626cd44..965e9de 100644
--- a/gopls/internal/regtest/codelens/codelens_test.go
+++ b/gopls/internal/regtest/codelens/codelens_test.go
@@ -12,9 +12,9 @@
 
 	. "golang.org/x/tools/gopls/internal/regtest"
 
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/fake"
 	"golang.org/x/tools/internal/lsp/protocol"
-	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/lsp/tests"
 	"golang.org/x/tools/internal/testenv"
 )
@@ -53,7 +53,7 @@
 		},
 		{
 			label:        "generate disabled",
-			enabled:      map[string]bool{source.CommandGenerate.Name: false},
+			enabled:      map[string]bool{string(command.Generate): false},
 			wantCodeLens: false,
 		},
 	}
@@ -161,7 +161,7 @@
 	t.Run("Upgrade individual dependency", func(t *testing.T) {
 		WithOptions(ProxyFiles(proxyWithLatest)).Run(t, shouldUpdateDep, func(t *testing.T, env *Env) {
 			env.OpenFile("go.mod")
-			env.ExecuteCodeLensCommand("go.mod", source.CommandCheckUpgrades)
+			env.ExecuteCodeLensCommand("go.mod", command.CheckUpgrades)
 			d := &protocol.PublishDiagnosticsParams{}
 			env.Await(OnceMet(env.DiagnosticAtRegexpWithMessage("go.mod", `require`, "can be upgraded"),
 				ReadDiagnostics("go.mod", d)))
@@ -219,7 +219,7 @@
 `
 	WithOptions(ProxyFiles(proxy)).Run(t, shouldRemoveDep, func(t *testing.T, env *Env) {
 		env.OpenFile("go.mod")
-		env.ExecuteCodeLensCommand("go.mod", source.CommandTidy)
+		env.ExecuteCodeLensCommand("go.mod", command.Tidy)
 		env.Await(env.DoneWithChangeWatchedFiles())
 		got := env.Editor.BufferText("go.mod")
 		const wantGoMod = `module mod.com
@@ -266,7 +266,7 @@
 		env.Await(env.DiagnosticAtRegexp("cgo.go", `C\.(fortytwo)`))
 
 		// Regenerate cgo, fixing the diagnostic.
-		env.ExecuteCodeLensCommand("cgo.go", source.CommandRegenerateCgo)
+		env.ExecuteCodeLensCommand("cgo.go", command.RegenerateCgo)
 		env.Await(EmptyDiagnostics("cgo.go"))
 	})
 }
@@ -301,7 +301,7 @@
 		Timeout(60*time.Second),
 	).Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
-		env.ExecuteCodeLensCommand("main.go", source.CommandToggleDetails)
+		env.ExecuteCodeLensCommand("main.go", command.GCDetails)
 		d := &protocol.PublishDiagnosticsParams{}
 		env.Await(
 			OnceMet(
@@ -334,7 +334,7 @@
 		env.Await(DiagnosticAt("main.go", 6, 12))
 
 		// Toggle the GC details code lens again so now it should be off.
-		env.ExecuteCodeLensCommand("main.go", source.CommandToggleDetails)
+		env.ExecuteCodeLensCommand("main.go", command.GCDetails)
 		env.Await(
 			EmptyDiagnostics("main.go"),
 		)
diff --git a/gopls/internal/regtest/misc/vendor_test.go b/gopls/internal/regtest/misc/vendor_test.go
index 0263090..8e76dd8 100644
--- a/gopls/internal/regtest/misc/vendor_test.go
+++ b/gopls/internal/regtest/misc/vendor_test.go
@@ -10,7 +10,6 @@
 	. "golang.org/x/tools/gopls/internal/regtest"
 
 	"golang.org/x/tools/internal/lsp/protocol"
-	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/testenv"
 )
 
@@ -71,10 +70,6 @@
 		env.Await(ReadDiagnostics("go.mod", d))
 		env.ApplyQuickFixes("go.mod", d.Diagnostics)
 
-		// Check for file changes when the command completes.
-		env.Await(CompletedWork(source.CommandVendor.Title, 1))
-		env.CheckForFileChanges()
-
 		// Confirm that there is no longer any inconsistent vendoring.
 		env.Await(
 			DiagnosticAt("a/a1.go", 6, 5),
diff --git a/gopls/internal/regtest/wrappers.go b/gopls/internal/regtest/wrappers.go
index bb614a5..fa9367e 100644
--- a/gopls/internal/regtest/wrappers.go
+++ b/gopls/internal/regtest/wrappers.go
@@ -9,9 +9,9 @@
 	"path"
 	"testing"
 
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/fake"
 	"golang.org/x/tools/internal/lsp/protocol"
-	"golang.org/x/tools/internal/lsp/source"
 	errors "golang.org/x/xerrors"
 )
 
@@ -299,7 +299,7 @@
 
 // ExecuteCodeLensCommand executes the command for the code lens matching the
 // given command name.
-func (e *Env) ExecuteCodeLensCommand(path string, cmd *source.Command) {
+func (e *Env) ExecuteCodeLensCommand(path string, cmd command.Command) {
 	lenses := e.CodeLens(path)
 	var lens protocol.CodeLens
 	var found bool
diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go
index 1b79ddf..5940651 100644
--- a/internal/lsp/cache/check.go
+++ b/internal/lsp/cache/check.go
@@ -21,6 +21,7 @@
 	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/event"
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/debug/tag"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
@@ -485,18 +486,16 @@
 			continue
 		}
 		direct := !strings.Contains(diag.Message, "error while importing")
-		args, err := source.MarshalArgs(pkg.compiledGoFiles[0].URI, direct, matches[1])
+		title := fmt.Sprintf("go get package %v", matches[1])
+		cmd, err := command.NewGoGetPackageCommand(title, command.GoGetPackageArgs{
+			URI:        protocol.URIFromSpanURI(pkg.compiledGoFiles[0].URI),
+			AddRequire: direct,
+			Pkg:        matches[1],
+		})
 		if err != nil {
 			return err
 		}
-		diag.SuggestedFixes = append(diag.SuggestedFixes, source.SuggestedFix{
-			Title: fmt.Sprintf("go get package %v", matches[1]),
-			Command: &protocol.Command{
-				Title:     fmt.Sprintf("go get package %v", matches[1]),
-				Command:   source.CommandGoGetPackage.ID(),
-				Arguments: args,
-			},
-		})
+		diag.SuggestedFixes = append(diag.SuggestedFixes, source.SuggestedFixFromCommand(cmd))
 	}
 	return nil
 }
diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go
index d47efe3..99f9a22 100644
--- a/internal/lsp/cache/mod.go
+++ b/internal/lsp/cache/mod.go
@@ -15,6 +15,7 @@
 	"golang.org/x/mod/module"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/gocommand"
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/debug/tag"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
@@ -285,7 +286,12 @@
 	disabledByGOPROXY := strings.Contains(goCmdError, "disabled by GOPROXY=off")
 	shouldAddDep := strings.Contains(goCmdError, "to add it")
 	if innermost != nil && (disabledByGOPROXY || shouldAddDep) {
-		args, err := source.MarshalArgs(fh.URI(), false, []string{fmt.Sprintf("%v@%v", innermost.Path, innermost.Version)})
+		title := fmt.Sprintf("Download %v@%v", innermost.Path, innermost.Version)
+		cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{
+			URI:        protocol.URIFromSpanURI(fh.URI()),
+			AddRequire: false,
+			GoCmdArgs:  []string{fmt.Sprintf("%v@%v", innermost.Path, innermost.Version)},
+		})
 		if err != nil {
 			return nil
 		}
@@ -294,19 +300,12 @@
 			msg = fmt.Sprintf("%v@%v has not been downloaded", innermost.Path, innermost.Version)
 		}
 		return &source.Diagnostic{
-			URI:      fh.URI(),
-			Range:    rng,
-			Severity: protocol.SeverityError,
-			Message:  msg,
-			Source:   source.ListError,
-			SuggestedFixes: []source.SuggestedFix{{
-				Title: fmt.Sprintf("Download %v@%v", innermost.Path, innermost.Version),
-				Command: &protocol.Command{
-					Title:     source.CommandAddDependency.Title,
-					Command:   source.CommandAddDependency.ID(),
-					Arguments: args,
-				},
-			}},
+			URI:            fh.URI(),
+			Range:          rng,
+			Severity:       protocol.SeverityError,
+			Message:        msg,
+			Source:         source.ListError,
+			SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd)},
 		}
 	}
 	diagSource := source.ListError
diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go
index 59f131c..e8a8547 100644
--- a/internal/lsp/cache/mod_tidy.go
+++ b/internal/lsp/cache/mod_tidy.go
@@ -18,6 +18,7 @@
 	"golang.org/x/mod/modfile"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/gocommand"
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/debug/tag"
 	"golang.org/x/tools/internal/lsp/diff"
 	"golang.org/x/tools/internal/lsp/protocol"
@@ -172,13 +173,15 @@
 	if err != nil {
 		return nil
 	}
-	args, err := source.MarshalArgs(protocol.URIFromSpanURI(fh.URI()))
-	if err != nil {
-		return nil
-	}
-
+	// TODO(rFindley): we shouldn't really need to call three constructors here.
+	//                 Reconsider this.
+	args := command.URIArg{protocol.URIFromSpanURI(fh.URI())}
 	switch {
 	case isInconsistentVendor:
+		cmd, err := command.NewVendorCommand("Run go mod vendor", args)
+		if err != nil {
+			return nil
+		}
 		return &source.Diagnostic{
 			URI:      fh.URI(),
 			Range:    rng,
@@ -186,17 +189,18 @@
 			Source:   source.ListError,
 			Message: `Inconsistent vendoring detected. Please re-run "go mod vendor".
 See https://github.com/golang/go/issues/39164 for more detail on this issue.`,
-			SuggestedFixes: []source.SuggestedFix{{
-				Title: source.CommandVendor.Title,
-				Command: &protocol.Command{
-					Command:   source.CommandVendor.ID(),
-					Title:     source.CommandVendor.Title,
-					Arguments: args,
-				},
-			}},
+			SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd)},
 		}
 
 	case isGoSumUpdates:
+		tidyCmd, err := command.NewTidyCommand("Run go mod tidy", args)
+		if err != nil {
+			return nil
+		}
+		updateCmd, err := command.NewUpdateGoSumCommand("Update go.sum", args)
+		if err != nil {
+			return nil
+		}
 		return &source.Diagnostic{
 			URI:      fh.URI(),
 			Range:    rng,
@@ -204,22 +208,8 @@
 			Source:   source.ListError,
 			Message:  `go.sum is out of sync with go.mod. Please update it or run "go mod tidy".`,
 			SuggestedFixes: []source.SuggestedFix{
-				{
-					Title: source.CommandTidy.Title,
-					Command: &protocol.Command{
-						Command:   source.CommandTidy.ID(),
-						Title:     source.CommandTidy.Title,
-						Arguments: args,
-					},
-				},
-				{
-					Title: source.CommandUpdateGoSum.Title,
-					Command: &protocol.Command{
-						Command:   source.CommandUpdateGoSum.ID(),
-						Title:     source.CommandUpdateGoSum.Title,
-						Arguments: args,
-					},
-				},
+				source.SuggestedFixFromCommand(tidyCmd),
+				source.SuggestedFixFromCommand(updateCmd),
 			},
 		}
 	}
@@ -385,24 +375,22 @@
 	if err != nil {
 		return nil, err
 	}
-	args, err := source.MarshalArgs(m.URI, onlyDiagnostic, req.Mod.Path)
+	title := fmt.Sprintf("Remove dependency: %s", req.Mod.Path)
+	cmd, err := command.NewRemoveDependencyCommand(title, command.RemoveDependencyArgs{
+		URI:            protocol.URIFromSpanURI(m.URI),
+		OnlyDiagnostic: onlyDiagnostic,
+		ModulePath:     req.Mod.Path,
+	})
 	if err != nil {
 		return nil, err
 	}
 	return &source.Diagnostic{
-		URI:      m.URI,
-		Range:    rng,
-		Severity: protocol.SeverityWarning,
-		Source:   source.ModTidyError,
-		Message:  fmt.Sprintf("%s is not used in this module", req.Mod.Path),
-		SuggestedFixes: []source.SuggestedFix{{
-			Title: fmt.Sprintf("Remove dependency: %s", req.Mod.Path),
-			Command: &protocol.Command{
-				Title:     source.CommandRemoveDependency.Title,
-				Command:   source.CommandRemoveDependency.ID(),
-				Arguments: args,
-			},
-		}},
+		URI:            m.URI,
+		Range:          rng,
+		Severity:       protocol.SeverityWarning,
+		Source:         source.ModTidyError,
+		Message:        fmt.Sprintf("%s is not used in this module", req.Mod.Path),
+		SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd)},
 	}, nil
 }
 
@@ -459,24 +447,22 @@
 			return nil, err
 		}
 	}
-	args, err := source.MarshalArgs(pm.Mapper.URI, !req.Indirect, []string{req.Mod.Path + "@" + req.Mod.Version})
+	title := fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path)
+	cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{
+		URI:        protocol.URIFromSpanURI(pm.Mapper.URI),
+		AddRequire: !req.Indirect,
+		GoCmdArgs:  []string{req.Mod.Path + "@" + req.Mod.Version},
+	})
 	if err != nil {
 		return nil, err
 	}
 	return &source.Diagnostic{
-		URI:      pm.Mapper.URI,
-		Range:    rng,
-		Severity: protocol.SeverityError,
-		Source:   source.ModTidyError,
-		Message:  fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
-		SuggestedFixes: []source.SuggestedFix{{
-			Title: fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path),
-			Command: &protocol.Command{
-				Title:     source.CommandAddDependency.Title,
-				Command:   source.CommandAddDependency.ID(),
-				Arguments: args,
-			},
-		}},
+		URI:            pm.Mapper.URI,
+		Range:          rng,
+		Severity:       protocol.SeverityError,
+		Source:         source.ModTidyError,
+		Message:        fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
+		SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd)},
 	}, nil
 }
 
diff --git a/internal/lsp/cmd/workspace.go b/internal/lsp/cmd/workspace.go
index 88531a7..a099599 100644
--- a/internal/lsp/cmd/workspace.go
+++ b/internal/lsp/cmd/workspace.go
@@ -9,6 +9,7 @@
 	"flag"
 	"fmt"
 
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/tool"
@@ -82,7 +83,11 @@
 		return err
 	}
 	defer conn.terminate(ctx)
-	params := &protocol.ExecuteCommandParams{Command: source.CommandGenerateGoplsMod.ID()}
+	cmd, err := command.NewGenerateGoplsModCommand("", command.URIArg{})
+	if err != nil {
+		return err
+	}
+	params := &protocol.ExecuteCommandParams{Command: cmd.Command, Arguments: cmd.Arguments}
 	if _, err := conn.ExecuteCommand(ctx, params); err != nil {
 		return fmt.Errorf("executing server command: %v", err)
 	}
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index b796433..1ab91fb 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -13,6 +13,7 @@
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/imports"
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/debug/tag"
 	"golang.org/x/tools/internal/lsp/mod"
 	"golang.org/x/tools/internal/lsp/protocol"
@@ -264,7 +265,7 @@
 		}
 		// If the suggested fix for the diagnostic is expected to be separate,
 		// see if there are any supported commands available.
-		if analyzer.Command != nil {
+		if analyzer.Fix != "" {
 			action, err := diagnosticToCommandCodeAction(ctx, snapshot, srcErr, &diag, protocol.QuickFix)
 			if err != nil {
 				return nil, nil, err
@@ -362,7 +363,7 @@
 		if !a.IsEnabled(snapshot.View()) {
 			continue
 		}
-		if a.Command == nil {
+		if a.Fix == "" {
 			event.Error(ctx, "convenienceFixes", fmt.Errorf("no suggested fixes for convenience analyzer %s", a.Analyzer.Name))
 			continue
 		}
@@ -399,10 +400,14 @@
 	if analyzer == nil {
 		return nil, fmt.Errorf("no convenience analyzer for source %s", sd.Source)
 	}
-	if analyzer.Command == nil {
-		return nil, fmt.Errorf("no command for convenience analyzer %s", analyzer.Analyzer.Name)
+	if analyzer.Fix == "" {
+		return nil, fmt.Errorf("no fix for convenience analyzer %s", analyzer.Analyzer.Name)
 	}
-	jsonArgs, err := source.MarshalArgs(sd.URI, sd.Range)
+	cmd, err := command.NewApplyFixCommand(sd.Message, command.ApplyFixArgs{
+		URI:   protocol.URIFromSpanURI(sd.URI),
+		Range: sd.Range,
+		Fix:   analyzer.Fix,
+	})
 	if err != nil {
 		return nil, err
 	}
@@ -414,11 +419,7 @@
 		Title:       sd.Message,
 		Kind:        kind,
 		Diagnostics: diagnostics,
-		Command: &protocol.Command{
-			Command:   analyzer.Command.ID(),
-			Title:     sd.Message,
-			Arguments: jsonArgs,
-		},
+		Command:     &cmd,
 	}, nil
 }
 
@@ -430,10 +431,6 @@
 	if err != nil {
 		return nil, err
 	}
-	jsonArgs, err := source.MarshalArgs(uri, rng)
-	if err != nil {
-		return nil, err
-	}
 	_, pgf, err := source.GetParsedFile(ctx, snapshot, fh, source.NarrowestPackage)
 	if err != nil {
 		return nil, errors.Errorf("getting file for Identifier: %w", err)
@@ -442,22 +439,36 @@
 	if err != nil {
 		return nil, err
 	}
-	var commands []*source.Command
+	puri := protocol.URIFromSpanURI(uri)
+	var commands []protocol.Command
 	if _, ok, _ := source.CanExtractFunction(snapshot.FileSet(), srng, pgf.Src, pgf.File); ok {
-		commands = append(commands, source.CommandExtractFunction)
+		cmd, err := command.NewApplyFixCommand("Extract to function", command.ApplyFixArgs{
+			URI:   puri,
+			Fix:   source.ExtractFunction,
+			Range: rng,
+		})
+		if err != nil {
+			return nil, err
+		}
+		commands = append(commands, cmd)
 	}
 	if _, _, ok, _ := source.CanExtractVariable(srng, pgf.File); ok {
-		commands = append(commands, source.CommandExtractVariable)
+		cmd, err := command.NewApplyFixCommand("Extract variable", command.ApplyFixArgs{
+			URI:   puri,
+			Fix:   source.ExtractVariable,
+			Range: rng,
+		})
+		if err != nil {
+			return nil, err
+		}
+		commands = append(commands, cmd)
 	}
 	var actions []protocol.CodeAction
-	for _, command := range commands {
+	for _, cmd := range commands {
 		actions = append(actions, protocol.CodeAction{
-			Title: command.Title,
-			Kind:  protocol.RefactorExtract,
-			Command: &protocol.Command{
-				Command:   command.ID(),
-				Arguments: jsonArgs,
-			},
+			Title:   cmd.Title,
+			Kind:    protocol.RefactorExtract,
+			Command: &cmd,
 		})
 	}
 	return actions, nil
@@ -574,17 +585,13 @@
 		return nil, nil
 	}
 
-	jsonArgs, err := source.MarshalArgs(uri, tests, benchmarks)
+	cmd, err := command.NewTestCommand("Run tests and benchmarks", protocol.URIFromSpanURI(uri), tests, benchmarks)
 	if err != nil {
 		return nil, err
 	}
 	return []protocol.CodeAction{{
-		Title: source.CommandTest.Name,
-		Kind:  protocol.GoTest,
-		Command: &protocol.Command{
-			Title:     source.CommandTest.Title,
-			Command:   source.CommandTest.ID(),
-			Arguments: jsonArgs,
-		},
+		Title:   cmd.Title,
+		Kind:    protocol.GoTest,
+		Command: &cmd,
 	}}, nil
 }
diff --git a/internal/lsp/code_lens.go b/internal/lsp/code_lens.go
index b1c6ba5..6e371fc 100644
--- a/internal/lsp/code_lens.go
+++ b/internal/lsp/code_lens.go
@@ -10,6 +10,7 @@
 	"sort"
 
 	"golang.org/x/tools/internal/event"
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/mod"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
@@ -21,26 +22,26 @@
 	if !ok {
 		return nil, err
 	}
-	var lensFuncs map[string]source.LensFunc
+	var lenses map[command.Command]source.LensFunc
 	switch fh.Kind() {
 	case source.Mod:
-		lensFuncs = mod.LensFuncs()
+		lenses = mod.LensFuncs()
 	case source.Go:
-		lensFuncs = source.LensFuncs()
+		lenses = source.LensFuncs()
 	default:
 		// Unsupported file kind for a code lens.
 		return nil, nil
 	}
 	var result []protocol.CodeLens
-	for lens, lf := range lensFuncs {
-		if !snapshot.View().Options().Codelenses[lens] {
+	for cmd, lf := range lenses {
+		if !snapshot.View().Options().Codelenses[string(cmd)] {
 			continue
 		}
 		added, err := lf(ctx, snapshot, fh)
 		// Code lens is called on every keystroke, so we should just operate in
 		// a best-effort mode, ignoring errors.
 		if err != nil {
-			event.Error(ctx, fmt.Sprintf("code lens %s failed", lens), err)
+			event.Error(ctx, fmt.Sprintf("code lens %s failed", cmd), err)
 			continue
 		}
 		result = append(result, added...)
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index 8b1b8e0..a11b8b3 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -18,6 +18,7 @@
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/gocommand"
 	"golang.org/x/tools/internal/lsp/cache"
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
@@ -37,12 +38,12 @@
 		return nil, fmt.Errorf("%s is not a supported command", params.Command)
 	}
 
-	cmd := &commandHandler{
+	handler := &commandHandler{
 		ctx:    ctx,
 		s:      s,
 		params: params,
 	}
-	return cmd.dispatch()
+	return command.Dispatch(params, handler)
 }
 
 type commandHandler struct {
@@ -114,106 +115,12 @@
 	return runcmd()
 }
 
-func (c *commandHandler) dispatch() (interface{}, error) {
-	switch c.params.Command {
-	case source.CommandFillStruct.ID(), source.CommandUndeclaredName.ID(),
-		source.CommandExtractVariable.ID(), source.CommandExtractFunction.ID():
-		var uri protocol.DocumentURI
-		var rng protocol.Range
-		if err := source.UnmarshalArgs(c.params.Arguments, &uri, &rng); err != nil {
-			return nil, err
-		}
-		err := c.ApplyFix(uri, rng)
-		return nil, err
-	case source.CommandTest.ID():
-		var uri protocol.DocumentURI
-		var tests, benchmarks []string
-		if err := source.UnmarshalArgs(c.params.Arguments, &uri, &tests, &benchmarks); err != nil {
-			return nil, err
-		}
-		err := c.RunTests(uri, tests, benchmarks)
-		return nil, err
-	case source.CommandGenerate.ID():
-		var uri protocol.DocumentURI
-		var recursive bool
-		if err := source.UnmarshalArgs(c.params.Arguments, &uri, &recursive); err != nil {
-			return nil, err
-		}
-		err := c.Generate(uri, recursive)
-		return nil, err
-	case source.CommandRegenerateCgo.ID():
-		var uri protocol.DocumentURI
-		if err := source.UnmarshalArgs(c.params.Arguments, &uri); err != nil {
-			return nil, err
-		}
-		return nil, c.RegenerateCgo(uri)
-	case source.CommandTidy.ID():
-		var uri protocol.DocumentURI
-		if err := source.UnmarshalArgs(c.params.Arguments, &uri); err != nil {
-			return nil, err
-		}
-		return nil, c.Tidy(uri)
-	case source.CommandVendor.ID():
-		var uri protocol.DocumentURI
-		if err := source.UnmarshalArgs(c.params.Arguments, &uri); err != nil {
-			return nil, err
-		}
-		return nil, c.Vendor(uri)
-	case source.CommandUpdateGoSum.ID():
-		var uri protocol.DocumentURI
-		if err := source.UnmarshalArgs(c.params.Arguments, &uri); err != nil {
-			return nil, err
-		}
-		return nil, c.UpdateGoSum(uri)
-	case source.CommandCheckUpgrades.ID():
-		var uri protocol.DocumentURI
-		var modules []string
-		if err := source.UnmarshalArgs(c.params.Arguments, &uri, &modules); err != nil {
-			return nil, err
-		}
-		return nil, c.CheckUpgrades(uri, modules)
-	case source.CommandAddDependency.ID(), source.CommandUpgradeDependency.ID():
-		var uri protocol.DocumentURI
-		var goCmdArgs []string
-		var addRequire bool
-		if err := source.UnmarshalArgs(c.params.Arguments, &uri, &addRequire, &goCmdArgs); err != nil {
-			return nil, err
-		}
-		return nil, c.GoGetModule(uri, addRequire, goCmdArgs)
-	case source.CommandRemoveDependency.ID():
-		var uri protocol.DocumentURI
-		var modulePath string
-		var onlyDiagnostic bool
-		if err := source.UnmarshalArgs(c.params.Arguments, &uri, &onlyDiagnostic, &modulePath); err != nil {
-			return nil, err
-		}
-		return nil, c.RemoveDependency(modulePath, uri, onlyDiagnostic)
-	case source.CommandGoGetPackage.ID():
-		var uri protocol.DocumentURI
-		var pkg string
-		var addRequire bool
-		if err := source.UnmarshalArgs(c.params.Arguments, &uri, &addRequire, &pkg); err != nil {
-			return nil, err
-		}
-		return nil, c.GoGetPackage(uri, addRequire, pkg)
-	case source.CommandToggleDetails.ID():
-		var uri protocol.DocumentURI
-		if err := source.UnmarshalArgs(c.params.Arguments, &uri); err != nil {
-			return nil, err
-		}
-		return nil, c.GCDetails(uri)
-	case source.CommandGenerateGoplsMod.ID():
-		return nil, c.GenerateGoplsMod()
-	}
-	return nil, fmt.Errorf("unsupported command: %s", c.params.Command)
-}
-
-func (c *commandHandler) ApplyFix(uri protocol.DocumentURI, rng protocol.Range) error {
+func (c *commandHandler) ApplyFix(args command.ApplyFixArgs) error {
 	return c.run(commandConfig{
 		// Note: no progress here. Applying fixes should be quick.
-		forURI: uri,
+		forURI: args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
-		edits, err := source.ApplyFix(ctx, c.params.Command, deps.snapshot, deps.fh, rng)
+		edits, err := source.ApplyFix(ctx, args.Fix, deps.snapshot, deps.fh, args.Range)
 		if err != nil {
 			return err
 		}
@@ -232,24 +139,24 @@
 	})
 }
 
-func (c *commandHandler) RegenerateCgo(uri protocol.DocumentURI) error {
+func (c *commandHandler) RegenerateCgo(args command.URIArg) error {
 	return c.run(commandConfig{
-		progress: source.CommandRegenerateCgo.Title,
+		progress: "Regenerating Cgo",
 	}, func(ctx context.Context, deps commandDeps) error {
 		mod := source.FileModification{
-			URI:    uri.SpanURI(),
+			URI:    args.URI.SpanURI(),
 			Action: source.InvalidateMetadata,
 		}
 		return c.s.didModifyFiles(c.ctx, []source.FileModification{mod}, FromRegenerateCgo)
 	})
 }
 
-func (c *commandHandler) CheckUpgrades(uri protocol.DocumentURI, modules []string) error {
+func (c *commandHandler) CheckUpgrades(args command.CheckUpgradesArgs) error {
 	return c.run(commandConfig{
-		forURI:   uri,
-		progress: source.CommandCheckUpgrades.Title,
+		forURI:   args.URI,
+		progress: "Checking for upgrades",
 	}, func(ctx context.Context, deps commandDeps) error {
-		upgrades, err := c.s.getUpgrades(ctx, deps.snapshot, uri.SpanURI(), modules)
+		upgrades, err := c.s.getUpgrades(ctx, deps.snapshot, args.URI.SpanURI(), args.Modules)
 		if err != nil {
 			return err
 		}
@@ -260,70 +167,78 @@
 	})
 }
 
-func (c *commandHandler) GoGetModule(uri protocol.DocumentURI, addRequire bool, goCmdArgs []string) error {
+func (c *commandHandler) AddDependency(args command.DependencyArgs) error {
+	return c.GoGetModule(args)
+}
+
+func (c *commandHandler) UpgradeDependency(args command.DependencyArgs) error {
+	return c.GoGetModule(args)
+}
+
+func (c *commandHandler) GoGetModule(args command.DependencyArgs) error {
 	return c.run(commandConfig{
 		requireSave: true,
 		progress:    "Running go get",
-		forURI:      uri,
+		forURI:      args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
-		return runGoGetModule(ctx, deps.snapshot, uri.SpanURI(), addRequire, goCmdArgs)
+		return runGoGetModule(ctx, deps.snapshot, args.URI.SpanURI(), args.AddRequire, args.GoCmdArgs)
 	})
 }
 
 // TODO(rFindley): UpdateGoSum, Tidy, and Vendor could probably all be one command.
 
-func (c *commandHandler) UpdateGoSum(uri protocol.DocumentURI) error {
+func (c *commandHandler) UpdateGoSum(args command.URIArg) error {
 	return c.run(commandConfig{
 		requireSave: true,
-		progress:    source.CommandUpdateGoSum.Title,
-		forURI:      uri,
+		progress:    "Updating go.sum",
+		forURI:      args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
-		return runSimpleGoCommand(ctx, deps.snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "list", []string{"all"})
+		return runSimpleGoCommand(ctx, deps.snapshot, source.UpdateUserModFile|source.AllowNetwork, args.URI.SpanURI(), "list", []string{"all"})
 	})
 }
 
-func (c *commandHandler) Tidy(uri protocol.DocumentURI) error {
+func (c *commandHandler) Tidy(args command.URIArg) error {
 	return c.run(commandConfig{
 		requireSave: true,
-		progress:    source.CommandTidy.Title,
-		forURI:      uri,
+		progress:    "Running go mod tidy",
+		forURI:      args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
-		return runSimpleGoCommand(ctx, deps.snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "mod", []string{"tidy"})
+		return runSimpleGoCommand(ctx, deps.snapshot, source.UpdateUserModFile|source.AllowNetwork, args.URI.SpanURI(), "mod", []string{"tidy"})
 	})
 }
 
-func (c *commandHandler) Vendor(uri protocol.DocumentURI) error {
+func (c *commandHandler) Vendor(args command.URIArg) error {
 	return c.run(commandConfig{
 		requireSave: true,
-		progress:    source.CommandVendor.Title,
-		forURI:      uri,
+		progress:    "Running go mod vendor",
+		forURI:      args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
-		return runSimpleGoCommand(ctx, deps.snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "mod", []string{"vendor"})
+		return runSimpleGoCommand(ctx, deps.snapshot, source.UpdateUserModFile|source.AllowNetwork, args.URI.SpanURI(), "mod", []string{"vendor"})
 	})
 }
 
-func (c *commandHandler) RemoveDependency(modulePath string, uri protocol.DocumentURI, onlyDiagnostic bool) error {
+func (c *commandHandler) RemoveDependency(args command.RemoveDependencyArgs) error {
 	return c.run(commandConfig{
 		requireSave: true,
-		progress:    source.CommandRemoveDependency.Title,
-		forURI:      uri,
+		progress:    "Removing dependency",
+		forURI:      args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
 		// If the module is tidied apart from the one unused diagnostic, we can
 		// run `go get module@none`, and then run `go mod tidy`. Otherwise, we
 		// must make textual edits.
 		// TODO(rstambler): In Go 1.17+, we will be able to use the go command
 		// without checking if the module is tidy.
-		if onlyDiagnostic {
-			if err := runGoGetModule(ctx, deps.snapshot, uri.SpanURI(), false, []string{modulePath + "@none"}); err != nil {
+		if args.OnlyDiagnostic {
+			if err := runGoGetModule(ctx, deps.snapshot, args.URI.SpanURI(), false, []string{args.ModulePath + "@none"}); err != nil {
 				return err
 			}
-			return runSimpleGoCommand(ctx, deps.snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "mod", []string{"tidy"})
+			return runSimpleGoCommand(ctx, deps.snapshot, source.UpdateUserModFile|source.AllowNetwork, args.URI.SpanURI(), "mod", []string{"tidy"})
 		}
 		pm, err := deps.snapshot.ParseMod(ctx, deps.fh)
 		if err != nil {
 			return err
 		}
-		edits, err := dropDependency(deps.snapshot, pm, modulePath)
+		edits, err := dropDependency(deps.snapshot, pm, args.ModulePath)
 		if err != nil {
 			return err
 		}
@@ -375,14 +290,22 @@
 	return source.ToProtocolEdits(pm.Mapper, diff)
 }
 
-func (c *commandHandler) RunTests(uri protocol.DocumentURI, tests, benchmarks []string) error {
+func (c *commandHandler) Test(uri protocol.DocumentURI, tests, benchmarks []string) error {
+	return c.RunTests(command.RunTestsArgs{
+		URI:        uri,
+		Tests:      tests,
+		Benchmarks: benchmarks,
+	})
+}
+
+func (c *commandHandler) RunTests(args command.RunTestsArgs) error {
 	return c.run(commandConfig{
 		async:       true,
-		progress:    source.CommandTest.Title,
+		progress:    "Running go test",
 		requireSave: true,
-		forURI:      uri,
+		forURI:      args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
-		if err := c.runTests(ctx, deps.snapshot, deps.work, uri, tests, benchmarks); err != nil {
+		if err := c.runTests(ctx, deps.snapshot, deps.work, args.URI, args.Tests, args.Benchmarks); err != nil {
 			if err := c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
 				Type:    protocol.Error,
 				Message: fmt.Sprintf("Running tests failed: %v", err),
@@ -472,22 +395,26 @@
 	})
 }
 
-func (c *commandHandler) Generate(uri protocol.DocumentURI, recursive bool) error {
+func (c *commandHandler) Generate(args command.GenerateArgs) error {
+	title := "Running go generate ."
+	if args.Recursive {
+		title = "Running go generate ./..."
+	}
 	return c.run(commandConfig{
 		requireSave: true,
-		progress:    source.CommandGenerate.Title,
-		forURI:      uri,
+		progress:    title,
+		forURI:      args.Dir,
 	}, func(ctx context.Context, deps commandDeps) error {
 		er := &eventWriter{ctx: ctx, operation: "generate"}
 
 		pattern := "."
-		if recursive {
+		if args.Recursive {
 			pattern = "./..."
 		}
 		inv := &gocommand.Invocation{
 			Verb:       "generate",
 			Args:       []string{"-x", pattern},
-			WorkingDir: uri.SpanURI().Filename(),
+			WorkingDir: args.Dir.SpanURI().Filename(),
 		}
 		stderr := io.MultiWriter(er, workDoneWriter{deps.work})
 		if err := deps.snapshot.RunGoCommandPiped(ctx, source.Normal, inv, er, stderr); err != nil {
@@ -497,22 +424,22 @@
 	})
 }
 
-func (c *commandHandler) GoGetPackage(puri protocol.DocumentURI, addRequire bool, pkg string) error {
+func (c *commandHandler) GoGetPackage(args command.GoGetPackageArgs) error {
 	return c.run(commandConfig{
-		forURI:   puri,
-		progress: source.CommandGoGetPackage.Title,
+		forURI:   args.URI,
+		progress: "Running go get",
 	}, func(ctx context.Context, deps commandDeps) error {
-		uri := puri.SpanURI()
+		uri := args.URI.SpanURI()
 		stdout, err := deps.snapshot.RunGoCommandDirect(ctx, source.WriteTemporaryModFile|source.AllowNetwork, &gocommand.Invocation{
 			Verb:       "list",
-			Args:       []string{"-f", "{{.Module.Path}}@{{.Module.Version}}", pkg},
+			Args:       []string{"-f", "{{.Module.Path}}@{{.Module.Version}}", args.Pkg},
 			WorkingDir: filepath.Dir(uri.Filename()),
 		})
 		if err != nil {
 			return err
 		}
 		ver := strings.TrimSpace(stdout.String())
-		return runGoGetModule(ctx, deps.snapshot, uri, addRequire, []string{ver})
+		return runGoGetModule(ctx, deps.snapshot, uri, args.AddRequire, []string{ver})
 	})
 }
 
@@ -563,12 +490,16 @@
 }
 
 func (c *commandHandler) GCDetails(uri protocol.DocumentURI) error {
+	return c.ToggleGCDetails(command.URIArg{URI: uri})
+}
+
+func (c *commandHandler) ToggleGCDetails(args command.URIArg) error {
 	return c.run(commandConfig{
 		requireSave: true,
-		progress:    source.CommandToggleDetails.Title,
-		forURI:      uri,
+		progress:    "Toggling GC Details",
+		forURI:      args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
-		pkgDir := span.URIFromPath(filepath.Dir(uri.SpanURI().Filename()))
+		pkgDir := span.URIFromPath(filepath.Dir(args.URI.SpanURI().Filename()))
 		c.s.gcOptimizationDetailsMu.Lock()
 		if _, ok := c.s.gcOptimizationDetails[pkgDir]; ok {
 			delete(c.s.gcOptimizationDetails, pkgDir)
@@ -582,10 +513,11 @@
 	})
 }
 
-func (c *commandHandler) GenerateGoplsMod() error {
+func (c *commandHandler) GenerateGoplsMod(args command.URIArg) error {
+	// TODO: go back to using URI
 	return c.run(commandConfig{
 		requireSave: true,
-		progress:    source.CommandGenerateGoplsMod.Title,
+		progress:    "Generating gopls.mod",
 	}, func(ctx context.Context, deps commandDeps) error {
 		views := c.s.session.Views()
 		if len(views) != 1 {
diff --git a/internal/lsp/command/command_gen.go b/internal/lsp/command/command_gen.go
index d3dbfd5..2bd2170 100644
--- a/internal/lsp/command/command_gen.go
+++ b/internal/lsp/command/command_gen.go
@@ -16,6 +16,44 @@
 	"golang.org/x/tools/internal/lsp/protocol"
 )
 
+const (
+	AddDependency     Command = "add_dependency"
+	ApplyFix          Command = "apply_fix"
+	CheckUpgrades     Command = "check_upgrades"
+	GCDetails         Command = "gc_details"
+	Generate          Command = "generate"
+	GenerateGoplsMod  Command = "generate_gopls_mod"
+	GoGetPackage      Command = "go_get_package"
+	RegenerateCgo     Command = "regenerate_cgo"
+	RemoveDependency  Command = "remove_dependency"
+	RunTests          Command = "run_tests"
+	Test              Command = "test"
+	Tidy              Command = "tidy"
+	ToggleGCDetails   Command = "toggle_gc_details"
+	UpdateGoSum       Command = "update_go_sum"
+	UpgradeDependency Command = "upgrade_dependency"
+	Vendor            Command = "vendor"
+)
+
+var Commands = []Command{
+	AddDependency,
+	ApplyFix,
+	CheckUpgrades,
+	GCDetails,
+	Generate,
+	GenerateGoplsMod,
+	GoGetPackage,
+	RegenerateCgo,
+	RemoveDependency,
+	RunTests,
+	Test,
+	Tidy,
+	ToggleGCDetails,
+	UpdateGoSum,
+	UpgradeDependency,
+	Vendor,
+}
+
 func Dispatch(params *protocol.ExecuteCommandParams, s Interface) (interface{}, error) {
 	switch params.Command {
 	case "gopls.add_dependency":
@@ -25,6 +63,13 @@
 		}
 		err := s.AddDependency(a0)
 		return nil, err
+	case "gopls.apply_fix":
+		var a0 ApplyFixArgs
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		err := s.ApplyFix(a0)
+		return nil, err
 	case "gopls.check_upgrades":
 		var a0 CheckUpgradesArgs
 		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
@@ -33,7 +78,7 @@
 		err := s.CheckUpgrades(a0)
 		return nil, err
 	case "gopls.gc_details":
-		var a0 URIArg
+		var a0 protocol.DocumentURI
 		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
 			return nil, err
 		}
@@ -97,6 +142,13 @@
 		}
 		err := s.Tidy(a0)
 		return nil, err
+	case "gopls.toggle_gc_details":
+		var a0 URIArg
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		err := s.ToggleGCDetails(a0)
+		return nil, err
 	case "gopls.update_go_sum":
 		var a0 URIArg
 		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
@@ -134,6 +186,18 @@
 	}, nil
 }
 
+func NewApplyFixCommand(title string, a0 ApplyFixArgs) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   "gopls.apply_fix",
+		Arguments: args,
+	}, nil
+}
+
 func NewCheckUpgradesCommand(title string, a0 CheckUpgradesArgs) (protocol.Command, error) {
 	args, err := MarshalArgs(a0)
 	if err != nil {
@@ -146,7 +210,7 @@
 	}, nil
 }
 
-func NewGCDetailsCommand(title string, a0 URIArg) (protocol.Command, error) {
+func NewGCDetailsCommand(title string, a0 protocol.DocumentURI) (protocol.Command, error) {
 	args, err := MarshalArgs(a0)
 	if err != nil {
 		return protocol.Command{}, err
@@ -254,6 +318,18 @@
 	}, nil
 }
 
+func NewToggleGCDetailsCommand(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_gc_details",
+		Arguments: args,
+	}, nil
+}
+
 func NewUpdateGoSumCommand(title string, a0 URIArg) (protocol.Command, error) {
 	args, err := MarshalArgs(a0)
 	if err != nil {
diff --git a/internal/lsp/command/commandmeta/meta.go b/internal/lsp/command/commandmeta/meta.go
index 5c5e57c..70b1fe2 100644
--- a/internal/lsp/command/commandmeta/meta.go
+++ b/internal/lsp/command/commandmeta/meta.go
@@ -31,6 +31,10 @@
 	Result types.Type
 }
 
+func (c *Command) ID() string {
+	return command.ID(c.Name)
+}
+
 type Field struct {
 	Name    string
 	Doc     string
@@ -44,14 +48,18 @@
 func Load() (*packages.Package, []*Command, error) {
 	pkgs, err := packages.Load(
 		&packages.Config{
-			Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedDeps,
+			Mode:       packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps,
+			BuildFlags: []string{"-tags=generate"},
 		},
 		"golang.org/x/tools/internal/lsp/command",
 	)
 	if err != nil {
-		return nil, nil, err
+		return nil, nil, fmt.Errorf("packages.Load: %v", err)
 	}
 	pkg := pkgs[0]
+	if len(pkg.Errors) > 0 {
+		return pkg, nil, pkg.Errors[0]
+	}
 
 	// For a bit of type safety, use reflection to get the interface name within
 	// the package scope.
@@ -138,7 +146,10 @@
 			obj2 := s.Field(i)
 			pkg2 := pkg
 			if obj2.Pkg() != pkg2.Types {
-				pkg2 = pkg.Imports[obj2.Pkg().Path()]
+				pkg2, ok = pkg.Imports[obj2.Pkg().Path()]
+				if !ok {
+					return nil, fmt.Errorf("missing import for %q: %q", pkg.ID, obj2.Pkg().Path())
+				}
 			}
 			node2, err := findField(pkg2, obj2.Pos())
 			if err != nil {
@@ -160,7 +171,7 @@
 //
 // The doc comment should be of the form: "MethodName: Title\nDocumentation"
 func splitDoc(text string) (title, doc string) {
-	docParts := strings.SplitN(doc, "\n", 2)
+	docParts := strings.SplitN(text, "\n", 2)
 	titleParts := strings.SplitN(docParts[0], ":", 2)
 	if len(titleParts) > 1 {
 		title = strings.TrimSpace(titleParts[1])
@@ -177,7 +188,7 @@
 	for i := range words {
 		words[i] = strings.ToLower(words[i])
 	}
-	return "gopls." + strings.Join(words, "_")
+	return strings.Join(words, "_")
 }
 
 // splitCamel splits s into words, according to camel-case word boundaries.
diff --git a/internal/lsp/command/generate/generate.go b/internal/lsp/command/generate/generate.go
index d31b437..eb24bbd 100644
--- a/internal/lsp/command/generate/generate.go
+++ b/internal/lsp/command/generate/generate.go
@@ -34,10 +34,22 @@
 	{{end}}
 )
 
+const (
+{{- range .Commands}}
+	{{.MethodName}} Command = "{{.Name}}"
+{{- end}}
+)
+
+var Commands = []Command {
+{{- range .Commands}}
+	{{.MethodName}},
+{{- end}}
+}
+
 func Dispatch(params *protocol.ExecuteCommandParams, s Interface) (interface{}, error) {
 	switch params.Command {
 	{{- range .Commands}}
-	case "{{.Name}}":
+	case "{{.ID}}":
 		{{- if .Args -}}
 			{{- range $i, $v := .Args}}
 		var a{{$i}} {{typeString $v.Type}}
@@ -61,7 +73,7 @@
 	}
 	return protocol.Command{
 		Title: title,
-		Command: "{{.Name}}",
+		Command: "{{.ID}}",
 		Arguments: args,
 	}, nil
 }
@@ -76,7 +88,7 @@
 func Generate() ([]byte, error) {
 	pkg, cmds, err := commandmeta.Load()
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("loading command data: %v", err)
 	}
 	qf := func(p *types.Package) string {
 		if p == pkg.Types {
diff --git a/internal/lsp/command/interface.go b/internal/lsp/command/interface.go
index 4f068fa..bc08526 100644
--- a/internal/lsp/command/interface.go
+++ b/internal/lsp/command/interface.go
@@ -28,11 +28,17 @@
 //     is considered the command 'Title'.
 //     TODO(rFindley): reconsider this -- Title may be unnecessary.
 type Interface interface {
+	// ApplyFix: Apply a fix
+	//
+	// Applies a fix to a region of source code.
+	ApplyFix(ApplyFixArgs) error
 	// Test: Run test(s) (legacy)
 	//
-	// Deprecated: use gopls.run_tests.
+	// Runs `go test` for a specific set of test or benchmark functions.
 	Test(protocol.DocumentURI, []string, []string) error
 
+	// TODO: deprecate Test in favor of RunTests below.
+
 	// Test: Run test(s)
 	//
 	// Runs `go test` for a specific set of test or benchmark functions.
@@ -91,7 +97,14 @@
 	// GCDetails: Toggle gc_details
 	//
 	// Toggle the calculation of gc annotations.
-	GCDetails(URIArg) error
+	GCDetails(protocol.DocumentURI) error
+
+	// TODO: deprecate GCDetails in favor of ToggleGCDetails below.
+
+	// ToggleGCDetails: Toggle gc_details
+	//
+	// Toggle the calculation of gc annotations.
+	ToggleGCDetails(URIArg) error
 
 	// GenerateGoplsMod: Generate gopls.mod
 	//
@@ -111,9 +124,8 @@
 }
 
 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
+	// Dir is the directory to generate.
+	Dir protocol.DocumentURI
 
 	// Recursive controls whether to generate recursively (go generate ./...)
 	Recursive bool
@@ -121,6 +133,12 @@
 
 // TODO(rFindley): document the rest of these once the docgen is fleshed out.
 
+type ApplyFixArgs struct {
+	Fix   string
+	URI   protocol.DocumentURI
+	Range protocol.Range
+}
+
 type URIArg struct {
 	URI protocol.DocumentURI
 }
@@ -143,6 +161,7 @@
 }
 
 type GoGetPackageArgs struct {
-	URI protocol.DocumentURI
-	Pkg string
+	URI        protocol.DocumentURI
+	Pkg        string
+	AddRequire bool
 }
diff --git a/internal/lsp/command/util.go b/internal/lsp/command/util.go
index c81aaf6..5915b9b 100644
--- a/internal/lsp/command/util.go
+++ b/internal/lsp/command/util.go
@@ -9,6 +9,17 @@
 	"fmt"
 )
 
+// ID returns the command name for use in the LSP.
+func ID(name string) string {
+	return "gopls." + name
+}
+
+type Command string
+
+func (c Command) ID() string {
+	return ID(string(c))
+}
+
 // MarshalArgs encodes the given arguments to json.RawMessages. This function
 // is used to construct arguments to a protocol.Command.
 //
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
index 9bd2028..ea77ffa 100644
--- a/internal/lsp/fake/editor.go
+++ b/internal/lsp/fake/editor.go
@@ -15,8 +15,8 @@
 	"sync"
 
 	"golang.org/x/tools/internal/jsonrpc2"
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/protocol"
-	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
 	errors "golang.org/x/xerrors"
 )
@@ -892,18 +892,22 @@
 // path. It does not report any resulting file changes as a watched file
 // change, so must be followed by a call to Workdir.CheckForFileChanges once
 // the generate command has completed.
+// TODO(rFindley): this shouldn't be necessary anymore. Delete it.
 func (e *Editor) RunGenerate(ctx context.Context, dir string) error {
 	if e.Server == nil {
 		return nil
 	}
 	absDir := e.sandbox.Workdir.AbsPath(dir)
-	jsonArgs, err := source.MarshalArgs(span.URIFromPath(absDir), false)
+	cmd, err := command.NewGenerateCommand("", command.GenerateArgs{
+		Dir:       protocol.URIFromSpanURI(span.URIFromPath(absDir)),
+		Recursive: false,
+	})
 	if err != nil {
 		return err
 	}
 	params := &protocol.ExecuteCommandParams{
-		Command:   source.CommandGenerate.ID(),
-		Arguments: jsonArgs,
+		Command:   cmd.Command,
+		Arguments: cmd.Arguments,
 	}
 	if _, err := e.ExecuteCommand(ctx, params); err != nil {
 		return fmt.Errorf("running generate: %v", err)
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 2318b04..2b82891 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -40,6 +40,7 @@
 	diagnostics map[span.URI][]*source.Diagnostic
 	ctx         context.Context
 	normalizers []tests.Normalizer
+	editRecv    chan map[span.URI]string
 }
 
 func testLSP(t *testing.T, datum *tests.Data) {
@@ -85,17 +86,19 @@
 		t.Fatal(err)
 	}
 	r := &runner{
-		server:      NewServer(session, testClient{}),
 		data:        datum,
 		ctx:         ctx,
 		normalizers: tests.CollectNormalizers(datum.Exported),
+		editRecv:    make(chan map[span.URI]string, 1),
 	}
+	r.server = NewServer(session, testClient{runner: r})
 	tests.Run(t, r, datum)
 }
 
 // testClient stubs any client functions that may be called by LSP functions.
 type testClient struct {
 	protocol.Client
+	runner *runner
 }
 
 // Trivially implement PublishDiagnostics so that we can call
@@ -104,6 +107,15 @@
 	return nil
 }
 
+func (c testClient) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResponse, error) {
+	res, err := applyTextDocumentEdits(c.runner, params.Edit.DocumentChanges)
+	if err != nil {
+		return nil, err
+	}
+	c.runner.editRecv <- res
+	return &protocol.ApplyWorkspaceEditResponse{Applied: true}, nil
+}
+
 func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests.CallHierarchyResult) {
 	mapper, err := r.data.Mapper(spn.URI())
 	if err != nil {
@@ -468,13 +480,6 @@
 		t.Fatal(err)
 	}
 
-	snapshot, release := view.Snapshot(r.ctx)
-	defer release()
-
-	fh, err := snapshot.GetVersionedFile(r.ctx, uri)
-	if err != nil {
-		t.Fatal(err)
-	}
 	m, err := r.data.Mapper(uri)
 	if err != nil {
 		t.Fatal(err)
@@ -533,14 +538,14 @@
 	}
 	var res map[span.URI]string
 	if cmd := action.Command; cmd != nil {
-		edits, err := source.ApplyFix(r.ctx, cmd.Command, snapshot, fh, rng)
+		_, err := r.server.ExecuteCommand(r.ctx, &protocol.ExecuteCommandParams{
+			Command:   action.Command.Command,
+			Arguments: action.Command.Arguments,
+		})
 		if err != nil {
 			t.Fatalf("error converting command %q to edits: %v", action.Command.Command, err)
 		}
-		res, err = applyTextDocumentEdits(r, edits)
-		if err != nil {
-			t.Fatal(err)
-		}
+		res = <-r.editRecv
 	} else {
 		res, err = applyTextDocumentEdits(r, action.Edit.DocumentChanges)
 		if err != nil {
@@ -559,18 +564,6 @@
 
 func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) {
 	uri := start.URI()
-	view, err := r.server.session.ViewOf(uri)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	snapshot, release := view.Snapshot(r.ctx)
-	defer release()
-
-	fh, err := snapshot.GetVersionedFile(r.ctx, uri)
-	if err != nil {
-		t.Fatal(err)
-	}
 	m, err := r.data.Mapper(uri)
 	if err != nil {
 		t.Fatal(err)
@@ -597,14 +590,14 @@
 	if len(actions) == 0 || len(actions) > 1 {
 		t.Fatalf("unexpected number of code actions, want 1, got %v", len(actions))
 	}
-	edits, err := source.ApplyFix(r.ctx, actions[0].Command.Command, snapshot, fh, rng)
+	_, err = r.server.ExecuteCommand(r.ctx, &protocol.ExecuteCommandParams{
+		Command:   actions[0].Command.Command,
+		Arguments: actions[0].Command.Arguments,
+	})
 	if err != nil {
 		t.Fatal(err)
 	}
-	res, err := applyTextDocumentEdits(r, edits)
-	if err != nil {
-		t.Fatal(err)
-	}
+	res := <-r.editRecv
 	for u, got := range res {
 		want := string(r.data.Golden("functionextraction_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) {
 			return []byte(got), nil
diff --git a/internal/lsp/mod/code_lens.go b/internal/lsp/mod/code_lens.go
index 88ffe84..f66a3e1 100644
--- a/internal/lsp/mod/code_lens.go
+++ b/internal/lsp/mod/code_lens.go
@@ -11,17 +11,18 @@
 	"path/filepath"
 
 	"golang.org/x/mod/modfile"
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
 )
 
 // LensFuncs returns the supported lensFuncs for go.mod files.
-func LensFuncs() map[string]source.LensFunc {
-	return map[string]source.LensFunc{
-		source.CommandUpgradeDependency.Name: upgradeLenses,
-		source.CommandTidy.Name:              tidyLens,
-		source.CommandVendor.Name:            vendorLens,
+func LensFuncs() map[command.Command]source.LensFunc {
+	return map[command.Command]source.LensFunc{
+		command.UpgradeDependency: upgradeLenses,
+		command.Tidy:              tidyLens,
+		command.Vendor:            vendorLens,
 	}
 }
 
@@ -34,19 +35,31 @@
 		// Nothing to upgrade.
 		return nil, nil
 	}
-	upgradeTransitiveArgs, err := source.MarshalArgs(fh.URI(), false, []string{"-u", "all"})
-	if err != nil {
-		return nil, err
-	}
 	var requires []string
 	for _, req := range pm.File.Require {
 		requires = append(requires, req.Mod.Path)
 	}
-	checkUpgradeArgs, err := source.MarshalArgs(fh.URI(), requires)
+	uri := protocol.URIFromSpanURI(fh.URI())
+	checkUpgrade, err := command.NewCheckUpgradesCommand("Check for upgrades", command.CheckUpgradesArgs{
+		URI:     uri,
+		Modules: requires,
+	})
 	if err != nil {
 		return nil, err
 	}
-	upgradeDirectArgs, err := source.MarshalArgs(fh.URI(), false, requires)
+	upgradeTransitive, err := command.NewUpgradeDependencyCommand("Upgrade transitive dependencies", command.DependencyArgs{
+		URI:        uri,
+		AddRequire: false,
+		GoCmdArgs:  []string{"-u", "all"},
+	})
+	if err != nil {
+		return nil, err
+	}
+	upgradeDirect, err := command.NewUpgradeDependencyCommand("Upgrade direct dependencies", command.DependencyArgs{
+		URI:        uri,
+		AddRequire: false,
+		GoCmdArgs:  requires,
+	})
 	if err != nil {
 		return nil, err
 	}
@@ -57,30 +70,9 @@
 	}
 
 	return []protocol.CodeLens{
-		{
-			Range: rng,
-			Command: protocol.Command{
-				Title:     "Check for upgrades",
-				Command:   source.CommandCheckUpgrades.ID(),
-				Arguments: checkUpgradeArgs,
-			},
-		},
-		{
-			Range: rng,
-			Command: protocol.Command{
-				Title:     "Upgrade transitive dependencies",
-				Command:   source.CommandUpgradeDependency.ID(),
-				Arguments: upgradeTransitiveArgs,
-			},
-		},
-		{
-			Range: rng,
-			Command: protocol.Command{
-				Title:     "Upgrade direct dependencies",
-				Command:   source.CommandUpgradeDependency.ID(),
-				Arguments: upgradeDirectArgs,
-			},
-		},
+		{Range: rng, Command: checkUpgrade},
+		{Range: rng, Command: upgradeTransitive},
+		{Range: rng, Command: upgradeDirect},
 	}, nil
 }
 
@@ -93,7 +85,8 @@
 		// Nothing to vendor.
 		return nil, nil
 	}
-	goModArgs, err := source.MarshalArgs(fh.URI())
+	uri := protocol.URIFromSpanURI(fh.URI())
+	cmd, err := command.NewTidyCommand("Run go mod tidy", command.URIArg{URI: uri})
 	if err != nil {
 		return nil, err
 	}
@@ -102,12 +95,8 @@
 		return nil, err
 	}
 	return []protocol.CodeLens{{
-		Range: rng,
-		Command: protocol.Command{
-			Title:     source.CommandTidy.Title,
-			Command:   source.CommandTidy.ID(),
-			Arguments: goModArgs,
-		},
+		Range:   rng,
+		Command: cmd,
 	}}, nil
 }
 
@@ -120,25 +109,19 @@
 	if err != nil {
 		return nil, err
 	}
-	goModArgs, err := source.MarshalArgs(fh.URI())
+	title := "Create vendor directory"
+	uri := protocol.URIFromSpanURI(fh.URI())
+	cmd, err := command.NewVendorCommand(title, command.URIArg{URI: uri})
 	if err != nil {
 		return nil, err
 	}
 	// Change the message depending on whether or not the module already has a
 	// vendor directory.
-	title := "Create vendor directory"
 	vendorDir := filepath.Join(filepath.Dir(fh.URI().Filename()), "vendor")
 	if info, _ := os.Stat(vendorDir); info != nil && info.IsDir() {
 		title = "Sync vendor directory"
 	}
-	return []protocol.CodeLens{{
-		Range: rng,
-		Command: protocol.Command{
-			Title:     title,
-			Command:   source.CommandVendor.ID(),
-			Arguments: goModArgs,
-		},
-	}}, nil
+	return []protocol.CodeLens{{Range: rng, Command: cmd}}, nil
 }
 
 func moduleStmtRange(fh source.FileHandle, pm *source.ParsedModule) (protocol.Range, error) {
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index dcd641e..041bf72 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -11,6 +11,7 @@
 	"fmt"
 
 	"golang.org/x/tools/internal/event"
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/debug/tag"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
@@ -65,24 +66,22 @@
 			return nil, err
 		}
 		// Upgrade to the exact version we offer the user, not the most recent.
-		args, err := source.MarshalArgs(fh.URI(), false, []string{req.Mod.Path + "@" + ver})
+		title := fmt.Sprintf("Upgrade to %v", ver)
+		cmd, err := command.NewUpgradeDependencyCommand(title, command.DependencyArgs{
+			URI:        protocol.URIFromSpanURI(fh.URI()),
+			AddRequire: false,
+			GoCmdArgs:  []string{req.Mod.Path + "@" + ver},
+		})
 		if err != nil {
 			return nil, err
 		}
 		diagnostics = append(diagnostics, &source.Diagnostic{
-			URI:      fh.URI(),
-			Range:    rng,
-			Severity: protocol.SeverityInformation,
-			Source:   source.UpgradeNotification,
-			Message:  fmt.Sprintf("%v can be upgraded", req.Mod.Path),
-			SuggestedFixes: []source.SuggestedFix{{
-				Title: fmt.Sprintf("Upgrade to %v", ver),
-				Command: &protocol.Command{
-					Title:     fmt.Sprintf("Upgrade to %v", ver),
-					Command:   source.CommandUpgradeDependency.ID(),
-					Arguments: args,
-				},
-			}},
+			URI:            fh.URI(),
+			Range:          rng,
+			Severity:       protocol.SeverityInformation,
+			Source:         source.UpgradeNotification,
+			Message:        fmt.Sprintf("%v can be upgraded", req.Mod.Path),
+			SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd)},
 		})
 	}
 
diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go
index 8a13428..1da2ccd 100755
--- a/internal/lsp/source/api_json.go
+++ b/internal/lsp/source/api_json.go
@@ -577,37 +577,37 @@
 					Keys: []EnumKey{
 						{
 							Name:    "\"gc_details\"",
-							Doc:     "gc_details controls calculation of gc annotations.\n",
+							Doc:     "Toggle the calculation of gc annotations.",
 							Default: "false",
 						},
 						{
 							Name:    "\"generate\"",
-							Doc:     "generate runs `go generate` for a given directory.\n",
+							Doc:     "Runs `go generate` for a given directory.",
 							Default: "true",
 						},
 						{
 							Name:    "\"regenerate_cgo\"",
-							Doc:     "regenerate_cgo regenerates cgo definitions.\n",
+							Doc:     "Regenerates cgo definitions.",
 							Default: "true",
 						},
 						{
 							Name:    "\"test\"",
-							Doc:     "test runs `go test` for a specific test function.\n",
+							Doc:     "Runs `go test` for a specific set of test or benchmark functions.",
 							Default: "false",
 						},
 						{
 							Name:    "\"tidy\"",
-							Doc:     "tidy runs `go mod tidy` for a module.\n",
+							Doc:     "Runs `go mod tidy` for a module.",
 							Default: "true",
 						},
 						{
 							Name:    "\"upgrade_dependency\"",
-							Doc:     "upgrade_dependency upgrades a dependency.\n",
+							Doc:     "Upgrades a dependency in the go.mod file for a module.",
 							Default: "true",
 						},
 						{
 							Name:    "\"vendor\"",
-							Doc:     "vendor runs `go mod vendor` for a module.\n",
+							Doc:     "Runs `go mod vendor` for a module.",
 							Default: "true",
 						},
 					},
@@ -675,124 +675,119 @@
 		{
 			Command: "gopls.add_dependency",
 			Title:   "Add dependency",
-			Doc:     "add_dependency adds a dependency.\n",
+			Doc:     "Adds a dependency to the go.mod file for a module.",
+		},
+		{
+			Command: "gopls.apply_fix",
+			Title:   "Apply a fix",
+			Doc:     "Applies a fix to a region of source code.",
 		},
 		{
 			Command: "gopls.check_upgrades",
 			Title:   "Check for upgrades",
-			Doc:     "check_upgrades checks for module upgrades.\n",
-		},
-		{
-			Command: "gopls.extract_function",
-			Title:   "Extract to function",
-			Doc:     "extract_function extracts statements to a function.\n",
-		},
-		{
-			Command: "gopls.extract_variable",
-			Title:   "Extract to variable",
-			Doc:     "extract_variable extracts an expression to a variable.\n",
-		},
-		{
-			Command: "gopls.fill_struct",
-			Title:   "Fill struct",
-			Doc:     "fill_struct is a gopls command to fill a struct with default\nvalues.\n",
+			Doc:     "Checks for module upgrades.",
 		},
 		{
 			Command: "gopls.gc_details",
 			Title:   "Toggle gc_details",
-			Doc:     "gc_details controls calculation of gc annotations.\n",
+			Doc:     "Toggle the calculation of gc annotations.",
 		},
 		{
 			Command: "gopls.generate",
 			Title:   "Run go generate",
-			Doc:     "generate runs `go generate` for a given directory.\n",
+			Doc:     "Runs `go generate` for a given directory.",
 		},
 		{
 			Command: "gopls.generate_gopls_mod",
 			Title:   "Generate gopls.mod",
-			Doc:     "generate_gopls_mod (re)generates the gopls.mod file.\n",
+			Doc:     "(Re)generate the gopls.mod file for a workspace.",
 		},
 		{
 			Command: "gopls.go_get_package",
 			Title:   "go get package",
-			Doc:     "go_get_package runs `go get` to fetch a package.\n",
+			Doc:     "Runs `go get` to fetch a package.",
 		},
 		{
 			Command: "gopls.regenerate_cgo",
 			Title:   "Regenerate cgo",
-			Doc:     "regenerate_cgo regenerates cgo definitions.\n",
+			Doc:     "Regenerates cgo definitions.",
 		},
 		{
 			Command: "gopls.remove_dependency",
 			Title:   "Remove dependency",
-			Doc:     "remove_dependency removes a dependency.\n",
+			Doc:     "Removes a dependency from the go.mod file of a module.",
+		},
+		{
+			Command: "gopls.run_tests",
+			Title:   "Run test(s)",
+			Doc:     "Runs `go test` for a specific set of test or benchmark functions.",
 		},
 		{
 			Command: "gopls.test",
-			Title:   "Run test(s)",
-			Doc:     "test runs `go test` for a specific test function.\n",
+			Title:   "Run test(s) (legacy)",
+			Doc:     "Runs `go test` for a specific set of test or benchmark functions.",
 		},
 		{
 			Command: "gopls.tidy",
 			Title:   "Run go mod tidy",
-			Doc:     "tidy runs `go mod tidy` for a module.\n",
+			Doc:     "Runs `go mod tidy` for a module.",
 		},
 		{
-			Command: "gopls.undeclared_name",
-			Title:   "Undeclared name",
-			Doc:     "undeclared_name adds a variable declaration for an undeclared\nname.\n",
+			Command: "gopls.toggle_gc_details",
+			Title:   "Toggle gc_details",
+			Doc:     "Toggle the calculation of gc annotations.",
 		},
 		{
 			Command: "gopls.update_go_sum",
 			Title:   "Update go.sum",
-			Doc:     "update_go_sum updates the go.sum file for a module.\n",
+			Doc:     "Updates the go.sum file for a module.",
 		},
 		{
 			Command: "gopls.upgrade_dependency",
 			Title:   "Upgrade dependency",
-			Doc:     "upgrade_dependency upgrades a dependency.\n",
+			Doc:     "Upgrades a dependency in the go.mod file for a module.",
 		},
 		{
 			Command: "gopls.vendor",
 			Title:   "Run go mod vendor",
-			Doc:     "vendor runs `go mod vendor` for a module.\n",
+			Doc:     "Runs `go mod vendor` for a module.",
 		},
 	},
 	Lenses: []*LensJSON{
 		{
 			Lens:  "gc_details",
 			Title: "Toggle gc_details",
-			Doc:   "gc_details controls calculation of gc annotations.\n",
+			Doc:   "Toggle the calculation of gc annotations.",
 		},
 		{
 			Lens:  "generate",
 			Title: "Run go generate",
-			Doc:   "generate runs `go generate` for a given directory.\n",
+			Doc:   "Runs `go generate` for a given directory.",
 		},
 		{
 			Lens:  "regenerate_cgo",
 			Title: "Regenerate cgo",
-			Doc:   "regenerate_cgo regenerates cgo definitions.\n",
+			Doc:   "Regenerates cgo definitions.",
 		},
 		{
 			Lens:  "test",
-			Title: "Run test(s)",
-			Doc:   "test runs `go test` for a specific test function.\n",
+			Title: "Run test(s) (legacy)",
+			Doc:   "Runs `go test` for a specific set of test or benchmark functions.",
 		},
 		{
 			Lens:  "tidy",
 			Title: "Run go mod tidy",
-			Doc:   "tidy runs `go mod tidy` for a module.\n",
+			Doc:   "Runs `go mod tidy` for a module.",
 		},
 		{
 			Lens:  "upgrade_dependency",
 			Title: "Upgrade dependency",
-			Doc:   "upgrade_dependency upgrades a dependency.\n",
+			Doc:   "Upgrades a dependency in the go.mod file for a module.",
 		},
 		{
 			Lens:  "vendor",
 			Title: "Run go mod vendor",
-			Doc:   "vendor runs `go mod vendor` for a module.\n",
+			Doc:   "Runs `go mod vendor` for a module.",
 		},
 	},
 	Analyzers: []*AnalyzerJSON{
diff --git a/internal/lsp/source/code_lens.go b/internal/lsp/source/code_lens.go
index 1dd66b4..0ab857a 100644
--- a/internal/lsp/source/code_lens.go
+++ b/internal/lsp/source/code_lens.go
@@ -13,6 +13,7 @@
 	"regexp"
 	"strings"
 
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/span"
 )
@@ -20,12 +21,12 @@
 type LensFunc func(context.Context, Snapshot, FileHandle) ([]protocol.CodeLens, error)
 
 // LensFuncs returns the supported lensFuncs for Go files.
-func LensFuncs() map[string]LensFunc {
-	return map[string]LensFunc{
-		CommandGenerate.Name:      goGenerateCodeLens,
-		CommandTest.Name:          runTestCodeLens,
-		CommandRegenerateCgo.Name: regenerateCgoLens,
-		CommandToggleDetails.Name: toggleDetailsCodeLens,
+func LensFuncs() map[command.Command]LensFunc {
+	return map[command.Command]LensFunc{
+		command.Generate:      goGenerateCodeLens,
+		command.Test:          runTestCodeLens,
+		command.RegenerateCgo: regenerateCgoLens,
+		command.GCDetails:     toggleDetailsCodeLens,
 	}
 }
 
@@ -41,34 +42,23 @@
 	if err != nil {
 		return nil, err
 	}
+	puri := protocol.URIFromSpanURI(fh.URI())
 	for _, fn := range fns.Tests {
-		jsonArgs, err := MarshalArgs(fh.URI(), []string{fn.Name}, nil)
+		cmd, err := command.NewTestCommand("run test", puri, []string{fn.Name}, nil)
 		if err != nil {
 			return nil, err
 		}
-		codeLens = append(codeLens, protocol.CodeLens{
-			Range: protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start},
-			Command: protocol.Command{
-				Title:     "run test",
-				Command:   CommandTest.ID(),
-				Arguments: jsonArgs,
-			},
-		})
+		rng := protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start}
+		codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: cmd})
 	}
 
 	for _, fn := range fns.Benchmarks {
-		jsonArgs, err := MarshalArgs(fh.URI(), nil, []string{fn.Name})
+		cmd, err := command.NewTestCommand("run benchmark", puri, nil, []string{fn.Name})
 		if err != nil {
 			return nil, err
 		}
-		codeLens = append(codeLens, protocol.CodeLens{
-			Range: protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start},
-			Command: protocol.Command{
-				Title:     "run benchmark",
-				Command:   CommandTest.ID(),
-				Arguments: jsonArgs,
-			},
-		})
+		rng := protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start}
+		codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: cmd})
 	}
 
 	if len(fns.Benchmarks) > 0 {
@@ -81,18 +71,15 @@
 		if err != nil {
 			return nil, err
 		}
-		args, err := MarshalArgs(fh.URI(), []string{}, fns.Benchmarks)
+		var benches []string
+		for _, fn := range fns.Benchmarks {
+			benches = append(benches, fn.Name)
+		}
+		cmd, err := command.NewTestCommand("run file benchmarks", puri, nil, benches)
 		if err != nil {
 			return nil, err
 		}
-		codeLens = append(codeLens, protocol.CodeLens{
-			Range: rng,
-			Command: protocol.Command{
-				Title:     "run file benchmarks",
-				Command:   CommandTest.ID(),
-				Arguments: args,
-			},
-		})
+		codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: cmd})
 	}
 	return codeLens, nil
 }
@@ -194,32 +181,18 @@
 			if err != nil {
 				return nil, err
 			}
-			dir := span.URIFromPath(filepath.Dir(fh.URI().Filename()))
-			nonRecursiveArgs, err := MarshalArgs(dir, false)
+			dir := protocol.URIFromSpanURI(span.URIFromPath(filepath.Dir(fh.URI().Filename())))
+			nonRecursiveCmd, err := command.NewGenerateCommand("run go generate", command.GenerateArgs{Dir: dir, Recursive: false})
 			if err != nil {
 				return nil, err
 			}
-			recursiveArgs, err := MarshalArgs(dir, true)
+			recursiveCmd, err := command.NewGenerateCommand("run go generate ./...", command.GenerateArgs{Dir: dir, Recursive: true})
 			if err != nil {
 				return nil, err
 			}
 			return []protocol.CodeLens{
-				{
-					Range: rng,
-					Command: protocol.Command{
-						Title:     "run go generate",
-						Command:   CommandGenerate.ID(),
-						Arguments: nonRecursiveArgs,
-					},
-				},
-				{
-					Range: rng,
-					Command: protocol.Command{
-						Title:     "run go generate ./...",
-						Command:   CommandGenerate.ID(),
-						Arguments: recursiveArgs,
-					},
-				},
+				{Range: rng, Command: recursiveCmd},
+				{Range: rng, Command: nonRecursiveCmd},
 			}, nil
 
 		}
@@ -245,20 +218,12 @@
 	if err != nil {
 		return nil, err
 	}
-	jsonArgs, err := MarshalArgs(fh.URI())
+	puri := protocol.URIFromSpanURI(fh.URI())
+	cmd, err := command.NewRegenerateCgoCommand("regenerate cgo definitions", command.URIArg{URI: puri})
 	if err != nil {
 		return nil, err
 	}
-	return []protocol.CodeLens{
-		{
-			Range: rng,
-			Command: protocol.Command{
-				Title:     "regenerate cgo definitions",
-				Command:   CommandRegenerateCgo.ID(),
-				Arguments: jsonArgs,
-			},
-		},
-	}, nil
+	return []protocol.CodeLens{{Range: rng, Command: cmd}}, nil
 }
 
 func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
@@ -270,16 +235,10 @@
 	if err != nil {
 		return nil, err
 	}
-	jsonArgs, err := MarshalArgs(fh.URI())
+	puri := protocol.URIFromSpanURI(fh.URI())
+	cmd, err := command.NewGCDetailsCommand("Toggle gc annotation details", puri)
 	if err != nil {
 		return nil, err
 	}
-	return []protocol.CodeLens{{
-		Range: rng,
-		Command: protocol.Command{
-			Title:     "Toggle gc annotation details",
-			Command:   CommandToggleDetails.ID(),
-			Arguments: jsonArgs,
-		},
-	}}, nil
+	return []protocol.CodeLens{{Range: rng, Command: cmd}}, nil
 }
diff --git a/internal/lsp/source/command.go b/internal/lsp/source/command.go
deleted file mode 100644
index 1edf154..0000000
--- a/internal/lsp/source/command.go
+++ /dev/null
@@ -1,243 +0,0 @@
-// Copyright 2020 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 source
-
-import (
-	"context"
-	"fmt"
-	"go/ast"
-	"go/token"
-	"go/types"
-
-	"golang.org/x/tools/go/analysis"
-	"golang.org/x/tools/internal/lsp/analysis/fillstruct"
-	"golang.org/x/tools/internal/lsp/analysis/undeclaredname"
-	"golang.org/x/tools/internal/lsp/protocol"
-	"golang.org/x/tools/internal/span"
-	errors "golang.org/x/xerrors"
-)
-
-type Command struct {
-	Title string
-	Name  string
-
-	// Async controls whether the command executes asynchronously.
-	Async bool
-}
-
-// CommandPrefix is the prefix of all command names gopls uses externally.
-const CommandPrefix = "gopls."
-
-// ID adds the CommandPrefix to the command name, in order to avoid
-// collisions with other language servers.
-func (c Command) ID() string {
-	return CommandPrefix + c.Name
-}
-
-// SuggestedFixFunc is a function used to get the suggested fixes for a given
-// gopls command, some of which are provided by go/analysis.Analyzers. Some of
-// the analyzers in internal/lsp/analysis are not efficient enough to include
-// suggested fixes with their diagnostics, so we have to compute them
-// separately. Such analyzers should provide a function with a signature of
-// SuggestedFixFunc.
-type SuggestedFixFunc func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error)
-
-// Commands are the commands currently supported by gopls.
-var Commands = []*Command{
-	CommandAddDependency,
-	CommandCheckUpgrades,
-	CommandExtractFunction,
-	CommandExtractVariable,
-	CommandFillStruct,
-	CommandToggleDetails, // gc_details
-	CommandGenerate,
-	CommandGenerateGoplsMod,
-	CommandGoGetPackage,
-	CommandRegenerateCgo,
-	CommandRemoveDependency,
-	CommandTest,
-	CommandTidy,
-	CommandUndeclaredName,
-	CommandUpdateGoSum,
-	CommandUpgradeDependency,
-	CommandVendor,
-}
-
-var (
-	// CommandTest runs `go test` for a specific test function.
-	CommandTest = &Command{
-		Name:  "test",
-		Title: "Run test(s)",
-		Async: true,
-	}
-
-	// CommandGenerate runs `go generate` for a given directory.
-	CommandGenerate = &Command{
-		Name:  "generate",
-		Title: "Run go generate",
-	}
-
-	// CommandTidy runs `go mod tidy` for a module.
-	CommandTidy = &Command{
-		Name:  "tidy",
-		Title: "Run go mod tidy",
-	}
-
-	// CommandVendor runs `go mod vendor` for a module.
-	CommandVendor = &Command{
-		Name:  "vendor",
-		Title: "Run go mod vendor",
-	}
-
-	// CommandGoGetPackage runs `go get` to fetch a package.
-	CommandGoGetPackage = &Command{
-		Name:  "go_get_package",
-		Title: "go get package",
-	}
-
-	// CommandUpdateGoSum updates the go.sum file for a module.
-	CommandUpdateGoSum = &Command{
-		Name:  "update_go_sum",
-		Title: "Update go.sum",
-	}
-
-	// CommandCheckUpgrades checks for module upgrades.
-	CommandCheckUpgrades = &Command{
-		Name:  "check_upgrades",
-		Title: "Check for upgrades",
-	}
-
-	// CommandAddDependency adds a dependency.
-	CommandAddDependency = &Command{
-		Name:  "add_dependency",
-		Title: "Add dependency",
-	}
-
-	// CommandUpgradeDependency upgrades a dependency.
-	CommandUpgradeDependency = &Command{
-		Name:  "upgrade_dependency",
-		Title: "Upgrade dependency",
-	}
-
-	// CommandRemoveDependency removes a dependency.
-	CommandRemoveDependency = &Command{
-		Name:  "remove_dependency",
-		Title: "Remove dependency",
-	}
-
-	// CommandRegenerateCgo regenerates cgo definitions.
-	CommandRegenerateCgo = &Command{
-		Name:  "regenerate_cgo",
-		Title: "Regenerate cgo",
-	}
-
-	// CommandToggleDetails controls calculation of gc annotations.
-	CommandToggleDetails = &Command{
-		Name:  "gc_details",
-		Title: "Toggle gc_details",
-	}
-
-	// CommandFillStruct is a gopls command to fill a struct with default
-	// values.
-	CommandFillStruct = &Command{
-		Name:  "fill_struct",
-		Title: "Fill struct",
-	}
-
-	// CommandUndeclaredName adds a variable declaration for an undeclared
-	// name.
-	CommandUndeclaredName = &Command{
-		Name:  "undeclared_name",
-		Title: "Undeclared name",
-	}
-
-	// CommandExtractVariable extracts an expression to a variable.
-	CommandExtractVariable = &Command{
-		Name:  "extract_variable",
-		Title: "Extract to variable",
-	}
-
-	// CommandExtractFunction extracts statements to a function.
-	CommandExtractFunction = &Command{
-		Name:  "extract_function",
-		Title: "Extract to function",
-	}
-
-	// CommandGenerateGoplsMod (re)generates the gopls.mod file.
-	CommandGenerateGoplsMod = &Command{
-		Name:  "generate_gopls_mod",
-		Title: "Generate gopls.mod",
-	}
-)
-
-// suggestedFixes maps a suggested fix command id to its handler.
-var suggestedFixes = map[string]SuggestedFixFunc{
-	CommandFillStruct.ID():      fillstruct.SuggestedFix,
-	CommandUndeclaredName.ID():  undeclaredname.SuggestedFix,
-	CommandExtractVariable.ID(): extractVariable,
-	CommandExtractFunction.ID(): extractFunction,
-}
-
-// ApplyFix applies the command's suggested fix to the given file and
-// range, returning the resulting edits.
-func ApplyFix(ctx context.Context, cmdid string, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) ([]protocol.TextDocumentEdit, error) {
-	handler, ok := suggestedFixes[cmdid]
-	if !ok {
-		return nil, fmt.Errorf("no suggested fix function for %s", cmdid)
-	}
-	fset, rng, src, file, m, pkg, info, err := getAllSuggestedFixInputs(ctx, snapshot, fh, pRng)
-	if err != nil {
-		return nil, err
-	}
-	fix, err := handler(fset, rng, src, file, pkg, info)
-	if err != nil {
-		return nil, err
-	}
-	if fix == nil {
-		return nil, nil
-	}
-
-	var edits []protocol.TextDocumentEdit
-	for _, edit := range fix.TextEdits {
-		rng := span.NewRange(fset, edit.Pos, edit.End)
-		spn, err := rng.Span()
-		if err != nil {
-			return nil, err
-		}
-		clRng, err := m.Range(spn)
-		if err != nil {
-			return nil, err
-		}
-		edits = append(edits, protocol.TextDocumentEdit{
-			TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
-				Version: fh.Version(),
-				TextDocumentIdentifier: protocol.TextDocumentIdentifier{
-					URI: protocol.URIFromSpanURI(fh.URI()),
-				},
-			},
-			Edits: []protocol.TextEdit{
-				{
-					Range:   clRng,
-					NewText: string(edit.NewText),
-				},
-			},
-		})
-	}
-	return edits, nil
-}
-
-// getAllSuggestedFixInputs is a helper function to collect all possible needed
-// inputs for an AppliesFunc or SuggestedFixFunc.
-func getAllSuggestedFixInputs(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, span.Range, []byte, *ast.File, *protocol.ColumnMapper, *types.Package, *types.Info, error) {
-	pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
-	if err != nil {
-		return nil, span.Range{}, nil, nil, nil, nil, nil, errors.Errorf("getting file for Identifier: %w", err)
-	}
-	rng, err := pgf.Mapper.RangeToSpanRange(pRng)
-	if err != nil {
-		return nil, span.Range{}, nil, nil, nil, nil, nil, err
-	}
-	return snapshot.FileSet(), rng, pgf.Src, pgf.File, pgf.Mapper, pkg.GetTypes(), pkg.GetTypesInfo(), nil
-}
diff --git a/internal/lsp/source/fix.go b/internal/lsp/source/fix.go
new file mode 100644
index 0000000..3918355
--- /dev/null
+++ b/internal/lsp/source/fix.go
@@ -0,0 +1,112 @@
+// Copyright 2020 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 source
+
+import (
+	"context"
+	"fmt"
+	"go/ast"
+	"go/token"
+	"go/types"
+
+	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/internal/lsp/analysis/fillstruct"
+	"golang.org/x/tools/internal/lsp/analysis/undeclaredname"
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/span"
+	errors "golang.org/x/xerrors"
+)
+
+// SuggestedFixFunc is a function used to get the suggested fixes for a given
+// gopls command, some of which are provided by go/analysis.Analyzers. Some of
+// the analyzers in internal/lsp/analysis are not efficient enough to include
+// suggested fixes with their diagnostics, so we have to compute them
+// separately. Such analyzers should provide a function with a signature of
+// SuggestedFixFunc.
+type SuggestedFixFunc func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error)
+
+const (
+	FillStruct      = "fill_struct"
+	UndeclaredName  = "undeclared_name"
+	ExtractVariable = "extract_variable"
+	ExtractFunction = "extract_function"
+)
+
+// suggestedFixes maps a suggested fix command id to its handler.
+var suggestedFixes = map[string]SuggestedFixFunc{
+	FillStruct:      fillstruct.SuggestedFix,
+	UndeclaredName:  undeclaredname.SuggestedFix,
+	ExtractVariable: extractVariable,
+	ExtractFunction: extractFunction,
+}
+
+func SuggestedFixFromCommand(cmd protocol.Command) SuggestedFix {
+	return SuggestedFix{
+		Title:   cmd.Title,
+		Command: &cmd,
+	}
+}
+
+// ApplyFix applies the command's suggested fix to the given file and
+// range, returning the resulting edits.
+func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) ([]protocol.TextDocumentEdit, error) {
+	handler, ok := suggestedFixes[fix]
+	if !ok {
+		return nil, fmt.Errorf("no suggested fix function for %s", fix)
+	}
+	fset, rng, src, file, m, pkg, info, err := getAllSuggestedFixInputs(ctx, snapshot, fh, pRng)
+	if err != nil {
+		return nil, err
+	}
+	suggestion, err := handler(fset, rng, src, file, pkg, info)
+	if err != nil {
+		return nil, err
+	}
+	if suggestion == nil {
+		return nil, nil
+	}
+
+	var edits []protocol.TextDocumentEdit
+	for _, edit := range suggestion.TextEdits {
+		rng := span.NewRange(fset, edit.Pos, edit.End)
+		spn, err := rng.Span()
+		if err != nil {
+			return nil, err
+		}
+		clRng, err := m.Range(spn)
+		if err != nil {
+			return nil, err
+		}
+		edits = append(edits, protocol.TextDocumentEdit{
+			TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
+				Version: fh.Version(),
+				TextDocumentIdentifier: protocol.TextDocumentIdentifier{
+					URI: protocol.URIFromSpanURI(fh.URI()),
+				},
+			},
+			Edits: []protocol.TextEdit{
+				{
+					Range:   clRng,
+					NewText: string(edit.NewText),
+				},
+			},
+		})
+	}
+	return edits, nil
+}
+
+// getAllSuggestedFixInputs is a helper function to collect all possible needed
+// inputs for an AppliesFunc or SuggestedFixFunc.
+func getAllSuggestedFixInputs(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, span.Range, []byte, *ast.File, *protocol.ColumnMapper, *types.Package, *types.Info, error) {
+	pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
+	if err != nil {
+		return nil, span.Range{}, nil, nil, nil, nil, nil, errors.Errorf("getting file for Identifier: %w", err)
+	}
+	rng, err := pgf.Mapper.RangeToSpanRange(pRng)
+	if err != nil {
+		return nil, span.Range{}, nil, nil, nil, nil, nil, err
+	}
+	return snapshot.FileSet(), rng, pgf.Src, pgf.File, pgf.Mapper, pkg.GetTypes(), pkg.GetTypesInfo(), nil
+}
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index 1584162..6758984 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -53,6 +53,7 @@
 	"golang.org/x/tools/internal/lsp/analysis/simplifyslice"
 	"golang.org/x/tools/internal/lsp/analysis/undeclaredname"
 	"golang.org/x/tools/internal/lsp/analysis/unusedparams"
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/diff"
 	"golang.org/x/tools/internal/lsp/diff/myers"
 	"golang.org/x/tools/internal/lsp/protocol"
@@ -70,7 +71,7 @@
 func DefaultOptions() *Options {
 	optionsOnce.Do(func() {
 		var commands []string
-		for _, c := range Commands {
+		for _, c := range command.Commands {
 			commands = append(commands, c.ID())
 		}
 		defaultOptions = &Options{
@@ -129,12 +130,12 @@
 						CompletionBudget: 100 * time.Millisecond,
 					},
 					Codelenses: map[string]bool{
-						CommandGenerate.Name:          true,
-						CommandRegenerateCgo.Name:     true,
-						CommandTidy.Name:              true,
-						CommandToggleDetails.Name:     false,
-						CommandUpgradeDependency.Name: true,
-						CommandVendor.Name:            true,
+						string(command.Generate):          true,
+						string(command.RegenerateCgo):     true,
+						string(command.Tidy):              true,
+						string(command.GCDetails):         false,
+						string(command.UpgradeDependency): true,
+						string(command.Vendor):            true,
 					},
 				},
 			},
@@ -676,8 +677,8 @@
 }
 
 func (o *Options) enableAllExperimentMaps() {
-	if _, ok := o.Codelenses[CommandToggleDetails.Name]; !ok {
-		o.Codelenses[CommandToggleDetails.Name] = true
+	if _, ok := o.Codelenses[string(command.GCDetails)]; !ok {
+		o.Codelenses[string(command.GCDetails)] = true
 	}
 	if _, ok := o.Analyses[unusedparams.Analyzer.Name]; !ok {
 		o.Analyses[unusedparams.Analyzer.Name] = true
@@ -1089,7 +1090,7 @@
 		undeclaredname.Analyzer.Name: {
 			Analyzer:   undeclaredname.Analyzer,
 			FixesError: undeclaredname.FixesError,
-			Command:    CommandUndeclaredName,
+			Fix:        UndeclaredName,
 			Enabled:    true,
 		},
 	}
@@ -1099,7 +1100,7 @@
 	return map[string]Analyzer{
 		fillstruct.Analyzer.Name: {
 			Analyzer: fillstruct.Analyzer,
-			Command:  CommandFillStruct,
+			Fix:      FillStruct,
 			Enabled:  true,
 		},
 	}
diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go
index e559e5c..c1ba082 100644
--- a/internal/lsp/source/util.go
+++ b/internal/lsp/source/util.go
@@ -6,8 +6,6 @@
 
 import (
 	"context"
-	"encoding/json"
-	"fmt"
 	"go/ast"
 	"go/printer"
 	"go/token"
@@ -293,50 +291,6 @@
 	return nil, nil, errors.Errorf("no file for %s in package %s", uri, pkg.ID())
 }
 
-// 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
-}
-
 // ImportPath returns the unquoted import path of s,
 // or "" if the path is not properly quoted.
 func ImportPath(s *ast.ImportSpec) string {
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 90b25e1..964d631 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -505,11 +505,11 @@
 	// the value of the Staticcheck setting overrides this field.
 	Enabled bool
 
-	// Command is the name of the command used to invoke the suggested fixes
-	// for the analyzer. It is non-nil if we expect this analyzer to provide
-	// its fix separately from its diagnostics. That is, we should apply the
-	// analyzer's suggested fixes through a Command, not a TextEdit.
-	Command *Command
+	// Fix is the name of the suggested fix name used to invoke the suggested
+	// fixes for the analyzer. It is non-empty if we expect this analyzer to
+	// provide its fix separately from its diagnostics. That is, we should apply
+	// the analyzer's suggested fixes through a Command, not a TextEdit.
+	Fix string
 
 	// If this is true, then we can apply the suggested fixes
 	// as part of a source.FixAll codeaction.
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index c99086a..e46c199 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -26,6 +26,7 @@
 	"golang.org/x/tools/go/expect"
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/go/packages/packagestest"
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/lsp/source/completion"
@@ -234,7 +235,7 @@
 		},
 		source.Sum: {},
 	}
-	o.UserOptions.Codelenses[source.CommandTest.Name] = true
+	o.UserOptions.Codelenses[string(command.Test)] = true
 	o.HoverKind = source.SynopsisDocumentation
 	o.InsertTextFormat = protocol.SnippetTextFormat
 	o.CompletionBudget = time.Minute