diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index ee5088230e..da8337190b 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -6,105 +6,81 @@ This document describes the LSP-level commands supported by `gopls`. They cannot ### **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. diff --git a/gopls/doc/generate.go b/gopls/doc/generate.go index 1eff8b979c..c6ba55b567 100644 --- a/gopls/doc/generate.go +++ b/gopls/doc/generate.go @@ -29,6 +29,8 @@ import ( "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 @@ func loadAPI() (*source.APIJSON, error) { // 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 valueDoc(name, value, doc string) string { } 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 df7258f7c8..f48dc3ffac 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -388,42 +388,35 @@ features are subject to change. 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. diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index 626cd44969..965e9de0b2 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -12,9 +12,9 @@ import ( . "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 @@ const ( }, { 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 @@ require golang.org/x/hello v1.3.3 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 @@ func main() { ` 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 @@ func Foo() { 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 @@ func main() { 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 @@ func main() { 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 026309095e..8e76dd82ca 100644 --- a/gopls/internal/regtest/misc/vendor_test.go +++ b/gopls/internal/regtest/misc/vendor_test.go @@ -10,7 +10,6 @@ import ( . "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 @@ func _() { 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 bb614a5ed5..fa9367e129 100644 --- a/gopls/internal/regtest/wrappers.go +++ b/gopls/internal/regtest/wrappers.go @@ -9,9 +9,9 @@ import ( "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 @@ func (e *Env) CodeLens(path string) []protocol.CodeLens { // 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 1b79ddfa6d..5940651f75 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -21,6 +21,7 @@ import ( "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 @@ func addGoGetFixes(ctx context.Context, snapshot source.Snapshot, pkg *pkg) erro 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 d47efe3de1..99f9a22969 100644 --- a/internal/lsp/cache/mod.go +++ b/internal/lsp/cache/mod.go @@ -15,6 +15,7 @@ import ( "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 @@ func (s *snapshot) matchErrorToModule(ctx context.Context, fh source.FileHandle, 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 @@ func (s *snapshot) matchErrorToModule(ctx context.Context, fh source.FileHandle, 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 59f131c6e0..e8a85478c7 100644 --- a/internal/lsp/cache/mod_tidy.go +++ b/internal/lsp/cache/mod_tidy.go @@ -18,6 +18,7 @@ import ( "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 @@ func (s *snapshot) parseModError(ctx context.Context, fh source.FileHandle, errT 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 @@ func (s *snapshot) parseModError(ctx context.Context, fh source.FileHandle, errT 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 @@ See https://github.com/golang/go/issues/39164 for more detail on this issue.`, 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 @@ func unusedDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, onlyDiagno 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 @@ func missingModuleDiagnostic(snapshot source.Snapshot, pm *source.ParsedModule, 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 88531a7778..a0995995a0 100644 --- a/internal/lsp/cmd/workspace.go +++ b/internal/lsp/cmd/workspace.go @@ -9,6 +9,7 @@ import ( "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 @@ func (c *generateWorkspaceMod) Run(ctx context.Context, args ...string) error { 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 b7964336e9..1ab91fb5c9 100644 --- a/internal/lsp/code_action.go +++ b/internal/lsp/code_action.go @@ -13,6 +13,7 @@ import ( "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 @@ func analysisFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Pac } // 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 @@ func convenienceFixes(ctx context.Context, snapshot source.Snapshot, pkg source. 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 @@ func diagnosticToCommandCodeAction(ctx context.Context, snapshot source.Snapshot 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 @@ func diagnosticToCommandCodeAction(ctx context.Context, snapshot source.Snapshot 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 @@ func extractionFixes(ctx context.Context, snapshot source.Snapshot, pkg source.P 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 @@ func extractionFixes(ctx context.Context, snapshot source.Snapshot, pkg source.P 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 @@ func goTest(ctx context.Context, snapshot source.Snapshot, uri span.URI, rng pro 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 b1c6ba597a..6e371fcc36 100644 --- a/internal/lsp/code_lens.go +++ b/internal/lsp/code_lens.go @@ -10,6 +10,7 @@ import ( "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 @@ func (s *Server) codeLens(ctx context.Context, params *protocol.CodeLensParams) 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 8b1b8e0c29..a11b8b3793 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -18,6 +18,7 @@ import ( "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 @@ func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCom 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 @@ func (c *commandHandler) run(cfg commandConfig, run commandFunc) (err error) { 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) ApplyFix(uri protocol.DocumentURI, rng protocol.Range) }) } -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) CheckUpgrades(uri protocol.DocumentURI, modules []strin }) } -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 @@ func dropDependency(snapshot source.Snapshot, pm *source.ParsedModule, modulePat 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) runTests(ctx context.Context, snapshot source.Snapshot, }) } -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) Generate(uri protocol.DocumentURI, recursive bool) erro }) } -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 (s *Server) getUpgrades(ctx context.Context, snapshot source.Snapshot, uri } 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) GCDetails(uri protocol.DocumentURI) error { }) } -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 d3dbfd5125..2bd2170e82 100644 --- a/internal/lsp/command/command_gen.go +++ b/internal/lsp/command/command_gen.go @@ -16,6 +16,44 @@ import ( "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 @@ func Dispatch(params *protocol.ExecuteCommandParams, s Interface) (interface{}, } 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 @@ func Dispatch(params *protocol.ExecuteCommandParams, s Interface) (interface{}, 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 @@ func Dispatch(params *protocol.ExecuteCommandParams, s Interface) (interface{}, } 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 @@ func NewAddDependencyCommand(title string, a0 DependencyArgs) (protocol.Command, }, 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 @@ func NewCheckUpgradesCommand(title string, a0 CheckUpgradesArgs) (protocol.Comma }, 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 @@ func NewTidyCommand(title string, a0 URIArg) (protocol.Command, error) { }, 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 5c5e57c803..70b1fe28b0 100644 --- a/internal/lsp/command/commandmeta/meta.go +++ b/internal/lsp/command/commandmeta/meta.go @@ -31,6 +31,10 @@ type Command struct { Result types.Type } +func (c *Command) ID() string { + return command.ID(c.Name) +} + type Field struct { Name string Doc string @@ -44,14 +48,18 @@ type Field struct { 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 @@ func (l *fieldLoader) loadField(pkg *packages.Package, node *ast.Field, obj *typ 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 @@ func (l *fieldLoader) loadField(pkg *packages.Package, node *ast.Field, obj *typ // // 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 @@ func lspName(methodName string) string { 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 d31b437489..eb24bbd353 100644 --- a/internal/lsp/command/generate/generate.go +++ b/internal/lsp/command/generate/generate.go @@ -34,10 +34,22 @@ import ( {{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 @@ func New{{.MethodName}}Command(title string, {{range $i, $v := .Args}}{{if $i}}, } return protocol.Command{ Title: title, - Command: "{{.Name}}", + Command: "{{.ID}}", Arguments: args, }, nil } @@ -76,7 +88,7 @@ type data struct { 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 4f068fa777..bc08526995 100644 --- a/internal/lsp/command/interface.go +++ b/internal/lsp/command/interface.go @@ -28,11 +28,17 @@ import "golang.org/x/tools/internal/lsp/protocol" // 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 @@ type Interface interface { // 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 RunTestsArgs struct { } 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 @@ type GenerateArgs struct { // 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 RemoveDependencyArgs struct { } 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 c81aaf6dbf..5915b9b70c 100644 --- a/internal/lsp/command/util.go +++ b/internal/lsp/command/util.go @@ -9,6 +9,17 @@ import ( "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 9bd2028877..ea77ffa2f5 100644 --- a/internal/lsp/fake/editor.go +++ b/internal/lsp/fake/editor.go @@ -15,8 +15,8 @@ import ( "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 @@ func (e *Editor) checkBufferPosition(path string, pos Pos) error { // 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 2318b043f8..2b828919d7 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -40,6 +40,7 @@ type runner struct { 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 @@ func testLSP(t *testing.T, datum *tests.Data) { 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 @@ func (c testClient) PublishDiagnostics(context.Context, *protocol.PublishDiagnos 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 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, 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 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, } 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) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, 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 @@ func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span 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) - if err != nil { - t.Fatal(err) - } - res, err := applyTextDocumentEdits(r, edits) + _, 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 := <-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 88ffe844fa..f66a3e17f9 100644 --- a/internal/lsp/mod/code_lens.go +++ b/internal/lsp/mod/code_lens.go @@ -11,17 +11,18 @@ import ( "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 @@ func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.File // 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 @@ func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.File } 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 @@ func tidyLens(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl // 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 @@ func tidyLens(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl 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 @@ func vendorLens(ctx context.Context, snapshot source.Snapshot, fh source.FileHan 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 dcd641e9a4..041bf72e6d 100644 --- a/internal/lsp/mod/diagnostics.go +++ b/internal/lsp/mod/diagnostics.go @@ -11,6 +11,7 @@ import ( "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 @@ func DiagnosticsForMod(ctx context.Context, snapshot source.Snapshot, fh source. 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 8a1342830a..1da2ccdc98 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -577,37 +577,37 @@ var GeneratedAPIJSON = &APIJSON{ 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 @@ var GeneratedAPIJSON = &APIJSON{ { 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 1dd66b469a..0ab857ac60 100644 --- a/internal/lsp/source/code_lens.go +++ b/internal/lsp/source/code_lens.go @@ -13,6 +13,7 @@ import ( "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 @@ import ( 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 @@ func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p 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 @@ func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p 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 @@ func goGenerateCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ( 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 @@ func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([ 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 @@ func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle 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 1edf1542c2..0000000000 --- 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 0000000000..391835593d --- /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 158416229d..6758984232 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -53,6 +53,7 @@ import ( "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 @@ var ( 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 @@ func DefaultOptions() *Options { 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) enableAllExperiments() { } 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 @@ func typeErrorAnalyzers() map[string]Analyzer { undeclaredname.Analyzer.Name: { Analyzer: undeclaredname.Analyzer, FixesError: undeclaredname.FixesError, - Command: CommandUndeclaredName, + Fix: UndeclaredName, Enabled: true, }, } @@ -1099,7 +1100,7 @@ func convenienceAnalyzers() map[string]Analyzer { 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 e559e5c1ea..c1ba082873 100644 --- a/internal/lsp/source/util.go +++ b/internal/lsp/source/util.go @@ -6,8 +6,6 @@ package source import ( "context" - "encoding/json" - "fmt" "go/ast" "go/printer" "go/token" @@ -293,50 +291,6 @@ func findFileInDeps(pkg Package, uri span.URI) (*ParsedGoFile, Package, error) { 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 90b25e199f..964d631140 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -505,11 +505,11 @@ type Analyzer struct { // 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 c99086ad3a..e46c19912e 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -26,6 +26,7 @@ import ( "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 @@ func DefaultOptions(o *source.Options) { }, 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