gopls/internal/test/integration: style tweaks to CodeAction Details: - Pass TriggerKind by value, not pointer. Define nil and zero as equivalent, always. Name the zero value: UnknownTrigger. Omit "kind" suffix in var names for brevity. - Add Workdir.EntireFile(path string) Location helper. - Env.CodeAction renamed CodeActionForFile. Env.CodeAction0 unsuffixed. Editor.CodeAction deleted. Editor.CodeAction0 unsuffixed. - Restore lost commentary. (Sorry, I was late to the code review of CL 590935.) Change-Id: I3fbe3e4e7567366b0742dcec44dc50539b9e9621 Reviewed-on: https://go-review.googlesource.com/c/tools/+/591176 Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com> Auto-Submit: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 8e8158c..2208822 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go
@@ -26,9 +26,14 @@ "golang.org/x/tools/internal/imports" ) -// CodeActions returns all code actions (edits and other commands) -// available for the selected range. -func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range, diagnostics []protocol.Diagnostic, want map[protocol.CodeActionKind]bool, triggerKind protocol.CodeActionTriggerKind) (actions []protocol.CodeAction, _ error) { +// CodeActions returns all wanted code actions (edits and other +// commands) available for the selected range. +// +// Depending on how the request was triggered, fewer actions may be +// offered, e.g. to avoid UI distractions after mere cursor motion. +// +// See ../protocol/codeactionkind.go for some code action theory. +func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range, diagnostics []protocol.Diagnostic, want map[protocol.CodeActionKind]bool, trigger protocol.CodeActionTriggerKind) (actions []protocol.CodeAction, _ error) { // Only compute quick fixes if there are any diagnostics to fix. wantQuickFixes := want[protocol.QuickFix] && len(diagnostics) > 0 @@ -138,7 +143,9 @@ actions = append(actions, rewrites...) } - if want[protocol.RefactorInline] && (triggerKind != protocol.CodeActionAutomatic || rng.Start != rng.End) { + // To avoid distraction (e.g. VS Code lightbulb), offer "inline" + // only after a selection or explicit menu operation. + if want[protocol.RefactorInline] && (trigger != protocol.CodeActionAutomatic || rng.Start != rng.End) { rewrites, err := getInlineCodeActions(pkg, pgf, rng, snapshot.Options()) if err != nil { return nil, err
diff --git a/gopls/internal/protocol/codeactionkind.go b/gopls/internal/protocol/codeactionkind.go index 8acba0e..f3d953f 100644 --- a/gopls/internal/protocol/codeactionkind.go +++ b/gopls/internal/protocol/codeactionkind.go
@@ -79,6 +79,11 @@ GoFreeSymbols CodeActionKind = "source.freesymbols" ) +// CodeActionUnknownTrigger indicates that the trigger for a +// CodeAction request is unknown. A missing +// CodeActionContext.TriggerKind should be treated as equivalent. +const CodeActionUnknownTrigger CodeActionTriggerKind = 0 + // A CodeLensSource identifies an (algorithmic) source of code lenses. type CodeLensSource string
diff --git a/gopls/internal/server/code_action.go b/gopls/internal/server/code_action.go index e79ae68..45f230e 100644 --- a/gopls/internal/server/code_action.go +++ b/gopls/internal/server/code_action.go
@@ -109,7 +109,7 @@ return actions, nil case file.Go: - // diagnostic-associated code actions (problematic code) + // diagnostic-bundled code actions // // The diagnostics already have a UI presence (e.g. squiggly underline); // the associated action may additionally show (in VS Code) as a lightbulb. @@ -120,11 +120,13 @@ if err != nil { return nil, err } - var triggerKind protocol.CodeActionTriggerKind - if k := params.Context.TriggerKind; k != nil { - triggerKind = *k + + // computed code actions (may include quickfixes from diagnostics) + trigger := protocol.CodeActionUnknownTrigger + if k := params.Context.TriggerKind; k != nil { // (some clients omit it) + trigger = *k } - moreActions, err := golang.CodeActions(ctx, snapshot, fh, params.Range, params.Context.Diagnostics, want, triggerKind) + moreActions, err := golang.CodeActions(ctx, snapshot, fh, params.Range, params.Context.Diagnostics, want, trigger) if err != nil { return nil, err }
diff --git a/gopls/internal/test/integration/bench/codeaction_test.go b/gopls/internal/test/integration/bench/codeaction_test.go index fe89500..679f2d4 100644 --- a/gopls/internal/test/integration/bench/codeaction_test.go +++ b/gopls/internal/test/integration/bench/codeaction_test.go
@@ -20,7 +20,7 @@ defer closeBuffer(b, env, test.file) env.AfterChange() - env.CodeAction(test.file, nil) // pre-warm + env.CodeActionForFile(test.file, nil) // pre-warm b.ResetTimer() @@ -29,7 +29,7 @@ } for i := 0; i < b.N; i++ { - env.CodeAction(test.file, nil) + env.CodeActionForFile(test.file, nil) } }) } @@ -44,7 +44,7 @@ env.EditBuffer(test.file, protocol.TextEdit{NewText: "// __TEST_PLACEHOLDER_0__\n"}) env.AfterChange() - env.CodeAction(test.file, nil) // pre-warm + env.CodeActionForFile(test.file, nil) // pre-warm b.ResetTimer() @@ -62,7 +62,7 @@ // Increment the placeholder text, to ensure cache misses. NewText: fmt.Sprintf("// __TEST_PLACEHOLDER_%d__\n", edits), }) - env.CodeAction(test.file, nil) + env.CodeActionForFile(test.file, nil) } }) }
diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go index f5d62b0..487a255 100644 --- a/gopls/internal/test/integration/fake/editor.go +++ b/gopls/internal/test/integration/fake/editor.go
@@ -933,7 +933,7 @@ // OrganizeImports requests and performs the source.organizeImports codeAction. func (e *Editor) OrganizeImports(ctx context.Context, path string) error { - loc := protocol.Location{URI: e.sandbox.Workdir.URI(path)} // zero Range => whole file + loc := e.sandbox.Workdir.EntireFile(path) _, err := e.applyCodeActions(ctx, loc, nil, protocol.SourceOrganizeImports) return err } @@ -1558,11 +1558,9 @@ // CodeAction executes a codeAction request on the server. // If loc.Range is zero, the whole file is implied. -func (e *Editor) CodeAction(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { - return e.CodeAction0(ctx, loc, diagnostics, nil) -} - -func (e *Editor) CodeAction0(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic, triggerKind *protocol.CodeActionTriggerKind) ([]protocol.CodeAction, error) { +// To reduce distraction, the trigger action (unknown, automatic, invoked) +// may affect what actions are offered. +func (e *Editor) CodeAction(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic, trigger protocol.CodeActionTriggerKind) ([]protocol.CodeAction, error) { if e.Server == nil { return nil, nil } @@ -1577,7 +1575,7 @@ TextDocument: e.TextDocumentIdentifier(path), Context: protocol.CodeActionContext{ Diagnostics: diagnostics, - TriggerKind: triggerKind, + TriggerKind: &trigger, }, Range: loc.Range, // may be zero }
diff --git a/gopls/internal/test/integration/fake/workdir.go b/gopls/internal/test/integration/fake/workdir.go index 4d21554..977bf54 100644 --- a/gopls/internal/test/integration/fake/workdir.go +++ b/gopls/internal/test/integration/fake/workdir.go
@@ -149,6 +149,11 @@ return w.RelPath(uri.Path()) } +// EntireFile returns the entire extent of the file named by the workdir-relative path. +func (w *Workdir) EntireFile(path string) protocol.Location { + return protocol.Location{URI: w.URI(path)} +} + // ReadFile reads a text file specified by a workdir-relative path. func (w *Workdir) ReadFile(path string) ([]byte, error) { backoff := 1 * time.Millisecond
diff --git a/gopls/internal/test/integration/misc/codeactions_test.go b/gopls/internal/test/integration/misc/codeactions_test.go index a6c2a8b..1a169c6 100644 --- a/gopls/internal/test/integration/misc/codeactions_test.go +++ b/gopls/internal/test/integration/misc/codeactions_test.go
@@ -40,7 +40,7 @@ check := func(filename string, wantKind ...protocol.CodeActionKind) { env.OpenFile(filename) loc := env.RegexpSearch(filename, `g\(\)`) - actions, err := env.Editor.CodeAction(env.Ctx, loc, nil) + actions, err := env.Editor.CodeAction(env.Ctx, loc, nil, protocol.CodeActionUnknownTrigger) if err != nil { t.Fatal(err) } @@ -87,23 +87,21 @@ Run(t, "", func(t *testing.T, env *Env) { env.CreateBuffer("main.go", vim1) - for _, triggerKind := range []protocol.CodeActionTriggerKind{0, protocol.CodeActionInvoked, protocol.CodeActionAutomatic} { - triggerKindPtr := &triggerKind - if triggerKind == 0 { - triggerKindPtr = nil - } - t.Run(fmt.Sprintf("trigger=%v", triggerKind), func(t *testing.T) { + for _, trigger := range []protocol.CodeActionTriggerKind{ + protocol.CodeActionUnknownTrigger, + protocol.CodeActionInvoked, + protocol.CodeActionAutomatic, + } { + t.Run(fmt.Sprintf("trigger=%v", trigger), func(t *testing.T) { for _, selectedRange := range []bool{false, true} { t.Run(fmt.Sprintf("range=%t", selectedRange), func(t *testing.T) { - pattern := env.RegexpSearch("main.go", "Func") - rng := pattern.Range + loc := env.RegexpSearch("main.go", "Func") if !selectedRange { // assume the cursor is placed at the beginning of `Func`, so end==start. - rng.End = rng.Start + loc.Range.End = loc.Range.Start } - loc := protocol.Location{URI: pattern.URI, Range: rng} - actions := env.CodeAction0("main.go", loc, nil, triggerKindPtr) - want := triggerKind != protocol.CodeActionAutomatic || selectedRange + actions := env.CodeAction(loc, nil, trigger) + want := trigger != protocol.CodeActionAutomatic || selectedRange if got := slices.ContainsFunc(actions, func(act protocol.CodeAction) bool { return act.Kind == protocol.RefactorInline }); got != want {
diff --git a/gopls/internal/test/integration/misc/extract_test.go b/gopls/internal/test/integration/misc/extract_test.go index 86afb45..ec13856 100644 --- a/gopls/internal/test/integration/misc/extract_test.go +++ b/gopls/internal/test/integration/misc/extract_test.go
@@ -30,7 +30,7 @@ Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") loc := env.RegexpSearch("main.go", `a := 5\n.*return a`) - actions, err := env.Editor.CodeAction(env.Ctx, loc, nil) + actions, err := env.Editor.CodeAction(env.Ctx, loc, nil, protocol.CodeActionUnknownTrigger) if err != nil { t.Fatal(err) }
diff --git a/gopls/internal/test/integration/misc/fix_test.go b/gopls/internal/test/integration/misc/fix_test.go index b3d86e1..acf896a 100644 --- a/gopls/internal/test/integration/misc/fix_test.go +++ b/gopls/internal/test/integration/misc/fix_test.go
@@ -115,7 +115,7 @@ ReadDiagnostics("main.go", &d), ) var quickFixes []*protocol.CodeAction - for _, act := range env.CodeAction("main.go", d.Diagnostics) { + for _, act := range env.CodeActionForFile("main.go", d.Diagnostics) { if act.Kind == protocol.QuickFix { act := act // remove in go1.22 quickFixes = append(quickFixes, &act) @@ -152,7 +152,7 @@ ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("external.go") - _, err := env.Editor.CodeAction(env.Ctx, env.RegexpSearch("external.go", "z"), nil) + _, err := env.Editor.CodeAction(env.Ctx, env.RegexpSearch("external.go", "z"), nil, protocol.CodeActionUnknownTrigger) if err != nil { t.Fatal(err) }
diff --git a/gopls/internal/test/integration/misc/imports_test.go b/gopls/internal/test/integration/misc/imports_test.go index 0e85a96..ebc1c0d 100644 --- a/gopls/internal/test/integration/misc/imports_test.go +++ b/gopls/internal/test/integration/misc/imports_test.go
@@ -149,7 +149,7 @@ env.OrganizeImports("main.go") // Assert no quick fixes. - for _, act := range env.CodeAction("main.go", nil) { + for _, act := range env.CodeActionForFile("main.go", nil) { if act.Kind == protocol.QuickFix { t.Errorf("unexpected quick fix action: %#v", act) } @@ -187,7 +187,7 @@ env.OrganizeImports("main.go") // Assert no quick fixes. - for _, act := range env.CodeAction("main.go", nil) { + for _, act := range env.CodeActionForFile("main.go", nil) { if act.Kind == protocol.QuickFix { t.Errorf("unexpected quick-fix action: %#v", act) }
diff --git a/gopls/internal/test/integration/misc/vuln_test.go b/gopls/internal/test/integration/misc/vuln_test.go index 6ba1ec8..1ed54a7 100644 --- a/gopls/internal/test/integration/misc/vuln_test.go +++ b/gopls/internal/test/integration/misc/vuln_test.go
@@ -519,7 +519,7 @@ for pattern, want := range wantVulncheckDiagnostics { modPathDiagnostics := testVulnDiagnostics(t, env, pattern, want, gotDiagnostics) - gotActions := env.CodeAction("go.mod", modPathDiagnostics) + gotActions := env.CodeActionForFile("go.mod", modPathDiagnostics) if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { t.Errorf("code actions for %q do not match, got %v, want %v\n%v\n", pattern, gotActions, want.codeActions, diff) continue @@ -716,7 +716,7 @@ modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics) // Check that the actions we get when including all diagnostics at a location return the same result - gotActions := env.CodeAction("go.mod", modPathDiagnostics) + gotActions := env.CodeActionForFile("go.mod", modPathDiagnostics) if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { t.Errorf("code actions for %q do not match, expected %v, got %v\n%v\n", mod, want.codeActions, gotActions, diff) continue @@ -839,7 +839,7 @@ for mod, want := range wantDiagnostics { modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics) // Check that the actions we get when including all diagnostics at a location return the same result - gotActions := env.CodeAction("go.mod", modPathDiagnostics) + gotActions := env.CodeActionForFile("go.mod", modPathDiagnostics) allActions = append(allActions, gotActions...) if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { t.Errorf("code actions for %q do not match, expected %v, got %v\n%v\n", mod, want.codeActions, gotActions, diff) @@ -889,7 +889,7 @@ t.Errorf("incorrect (severity, source) for %q, want (%s, %s) got (%s, %s)\n", w.msg, w.severity, w.source, diag.Severity, diag.Source) } // Check expected code actions appear. - gotActions := env.CodeAction("go.mod", []protocol.Diagnostic{*diag}) + gotActions := env.CodeActionForFile("go.mod", []protocol.Diagnostic{*diag}) if diff := diffCodeActions(gotActions, w.codeActions); diff != "" { t.Errorf("code actions for %q do not match, want %v, got %v\n%v\n", w.msg, w.codeActions, gotActions, diff) continue
diff --git a/gopls/internal/test/integration/misc/webserver_test.go b/gopls/internal/test/integration/misc/webserver_test.go index c4af449..851cc68 100644 --- a/gopls/internal/test/integration/misc/webserver_test.go +++ b/gopls/internal/test/integration/misc/webserver_test.go
@@ -198,7 +198,7 @@ // Invoke the "View package documentation" code // action to start the server. var docAction *protocol.CodeAction - actions := env.CodeAction(filename, nil) + actions := env.CodeActionForFile(filename, nil) for _, act := range actions { if act.Title == "View package documentation" { docAction = &act @@ -254,7 +254,7 @@ // Invoke the "Show free symbols" code // action to start the server. loc := env.RegexpSearch("a/a.go", "«((?:.|\n)*)»") - actions, err := env.Editor.CodeAction(env.Ctx, loc, nil) + actions, err := env.Editor.CodeAction(env.Ctx, loc, nil, protocol.CodeActionUnknownTrigger) if err != nil { t.Fatalf("CodeAction: %v", err) }
diff --git a/gopls/internal/test/integration/wrappers.go b/gopls/internal/test/integration/wrappers.go index 0982f8a..8c5e35e 100644 --- a/gopls/internal/test/integration/wrappers.go +++ b/gopls/internal/test/integration/wrappers.go
@@ -207,7 +207,7 @@ // ApplyQuickFixes processes the quickfix codeAction, calling t.Fatal on any error. func (e *Env) ApplyQuickFixes(path string, diagnostics []protocol.Diagnostic) { e.T.Helper() - loc := protocol.Location{URI: e.Sandbox.Workdir.URI(path)} // zero Range => whole file + loc := e.Sandbox.Workdir.EntireFile(path) if err := e.Editor.ApplyQuickFixes(e.Ctx, loc, diagnostics); err != nil { e.T.Fatal(err) } @@ -224,7 +224,7 @@ // GetQuickFixes returns the available quick fix code actions. func (e *Env) GetQuickFixes(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction { e.T.Helper() - loc := protocol.Location{URI: e.Sandbox.Workdir.URI(path)} // zero Range => whole file + loc := e.Sandbox.Workdir.EntireFile(path) actions, err := e.Editor.GetQuickFixes(e.Ctx, loc, diagnostics) if err != nil { e.T.Fatal(err) @@ -533,16 +533,17 @@ } } -// CodeAction calls textDocument/codeAction for the given path, and calls -// t.Fatal if there are errors. -func (e *Env) CodeAction(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction { - loc := protocol.Location{URI: e.Sandbox.Workdir.URI(path)} // no Range => whole file - return e.CodeAction0(path, loc, diagnostics, nil) +// CodeActionForFile calls textDocument/codeAction for the entire +// file, and calls t.Fatal if there were errors. +func (e *Env) CodeActionForFile(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction { + return e.CodeAction(e.Sandbox.Workdir.EntireFile(path), diagnostics, protocol.CodeActionUnknownTrigger) } -func (e *Env) CodeAction0(path string, loc protocol.Location, diagnostics []protocol.Diagnostic, triggerKind *protocol.CodeActionTriggerKind) []protocol.CodeAction { +// CodeAction calls textDocument/codeAction for a selection, +// and calls t.Fatal if there were errors. +func (e *Env) CodeAction(loc protocol.Location, diagnostics []protocol.Diagnostic, trigger protocol.CodeActionTriggerKind) []protocol.CodeAction { e.T.Helper() - actions, err := e.Editor.CodeAction0(e.Ctx, loc, diagnostics, triggerKind) + actions, err := e.Editor.CodeAction(e.Ctx, loc, diagnostics, trigger) if err != nil { e.T.Fatal(err) }