gopls/codeaction: Add dialog tests for editing struct tags
This CL makes it possible to test dialogs in code action marker
tests. The key message from clients to gopls is 'command/resolve'.
Clients with full control of their communications with gopls
can get to server.ResolveCommand() directly, but vscode-go cannot.
The new code for codeActionMarker() simulates a vscode-go-like client,
as this seems to be the most stressing case.
diff --git a/gopls/internal/test/marker/codeaction_test.go b/gopls/internal/test/marker/codeaction_test.go
new file mode 100644
index 0000000..b9e5adf
--- /dev/null
+++ b/gopls/internal/test/marker/codeaction_test.go
@@ -0,0 +1,353 @@
+// Copyright 2026 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 marker
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "path/filepath"
+ "regexp"
+
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
+ "golang.org/x/tools/gopls/internal/test/integration"
+ "golang.org/x/tools/internal/expect"
+)
+
+func codeActionMarker(mark marker, loc protocol.Location, kind string) {
+ if !exactlyOneNamedArg(mark, "action", "edit", "result", "err") {
+ return
+ }
+
+ if end := namedArgFunc(mark, "end", convertNamedArgLocation, protocol.Location{}); end.URI != "" {
+ if end.URI != loc.URI {
+ mark.errorf("end marker is in a different file (%s)", filepath.Base(loc.URI.Path()))
+ return
+ }
+ loc.Range.End = end.Range.End
+ }
+
+ var (
+ edit = namedArg(mark, "edit", expect.Identifier(""))
+ result = namedArg(mark, "result", expect.Identifier(""))
+ wantAction = namedArg(mark, "action", expect.Identifier(""))
+ wantErr = namedArgFunc(mark, "err", convertStringMatcher, stringMatcher{})
+ )
+
+ var diag *protocol.Diagnostic
+ if re := namedArg(mark, "diag", (*regexp.Regexp)(nil)); re != nil {
+ d, ok := removeDiagnostic(mark, loc, false, re)
+ if !ok {
+ mark.errorf("no diagnostic at %v matches %q", loc, re)
+ return
+ }
+ diag = &d
+ }
+
+ action, err := resolveCodeAction(mark.run.env, loc.URI, loc.Range, protocol.CodeActionKind(kind), diag)
+ if err != nil {
+ if !wantErr.empty() {
+ wantErr.checkErr(mark, err)
+ } else {
+ mark.errorf("resolveCodeAction failed: %v", err)
+ }
+ return
+ }
+
+ // If when 'action' is set, we simply compare the action, and don't apply it.
+ if wantAction != "" {
+ g := mark.getGolden(wantAction)
+ if action == nil {
+ mark.errorf("no matching codeAction")
+ return
+ }
+ data, err := json.MarshalIndent(action, "", "\t")
+ if err != nil {
+ mark.errorf("failed to marshal codeaction: %v", err)
+ return
+ }
+ data = bytes.ReplaceAll(data, []byte(mark.run.env.Sandbox.Workdir.RootURI()), []byte("$WORKDIR"))
+ compareGolden(mark, data, g)
+ return
+ }
+
+ var changes []protocol.DocumentChange
+ if namedArg(mark, "form0", "") != "" { // wants a dialog
+ // so simulate a dialog-supporting LSP client
+ // (note that the test file should have a capabilities.json file containing
+ // "experimental":{"interactiveInputTypes":["string","bool","number","enum"]}})
+ // The client sends one codeAction/resolve and then two workspace/executeCommand
+ // The first is to get the form for the dialog
+ // The second is to return the user's choices
+ // This deliberately simulates what vscode does, instead of direclty calling ResolveCommand
+ naction, err := mark.run.env.Editor.Server.ResolveCodeAction(mark.run.env.Ctx, action)
+ if err != nil {
+ mark.errorf("ResolveCodeAction failed: %v", err)
+ return
+ }
+ var modifytags command.ModifyTagsArgs
+ if err := json.Unmarshal(naction.Command.Arguments[0], &modifytags); err != nil {
+ mark.errorf("Unmarshal failed, expected command.ModyfyTagsArgs:%v", err)
+ return
+ }
+ yy := protocol.ExecuteCommandParams{
+ Command: "gopls.modify_tags",
+ Arguments: naction.Command.Arguments,
+ }
+ check1, err := json.Marshal(&yy)
+ if err != nil {
+ mark.errorf("marshal(ExecutecommandParams) failed: %v", err)
+ return
+ }
+ zz := command.LSPArgs{
+ Method: "command/resolve",
+ Param: check1, // json.RawMessage(check1) might be clearer, or not
+ }
+ check2, err := json.Marshal(&zz)
+ if err != nil {
+ mark.errorf("marshal(LSPArgs) failed: %v", err)
+ return
+ }
+ ww := &protocol.ExecuteCommandParams{
+ Command: "gopls.lsp",
+ Arguments: []json.RawMessage{check2},
+ }
+ got, err := mark.run.env.Editor.Server.ExecuteCommand(mark.run.env.Ctx, ww)
+ if err != nil {
+ mark.errorf("ExecuteCommand failed: %v", err)
+ return
+ }
+ useful, ok := got.(map[string]any)
+ if !ok {
+ mark.errorf("unexpected type %T, expected map[string]any", got)
+ return
+ }
+ // catch the changes from the forthcoming ExecuteCommand
+ restore := mark.run.env.Editor.Client().SetApplyEditHandler(func(ctx context.Context, wsedit *protocol.WorkspaceEdit) error {
+ changes = append(changes, wsedit.DocumentChanges...)
+ return nil
+ })
+ defer restore()
+ switch kind {
+ case "refactor.rewrite.removeTags":
+ modifytags.Remove = mark.note.NamedArgs["form0"].(string)
+ case "refactor.rewrite.addTags":
+ modifytags.Add = mark.note.NamedArgs["form0"].(string)
+ modifytags.Transform = mark.note.NamedArgs["form1"].(string)
+ default:
+ mark.errorf("unexpected kind %q", kind)
+ return
+ }
+ want, err := json.Marshal(modifytags)
+ if err != nil {
+ log.Fatal(err)
+ }
+ cmd := &protocol.ExecuteCommandParams{
+ Command: useful["command"].(string),
+ Arguments: []json.RawMessage{want},
+ }
+ got, err = mark.run.env.Editor.Server.ExecuteCommand(mark.run.env.Ctx, cmd)
+ if err != nil {
+ mark.errorf("ExecuteCommand failed: %v", err)
+ return
+ } // expect nil return. Side effect is to change the file
+ } else {
+ changes, err = applyCodeAction(mark.run.env, action)
+ if err != nil {
+ if !wantErr.empty() {
+ wantErr.checkErr(mark, err)
+ } else {
+ mark.errorf("codeAction failed: %v", err)
+ }
+ return
+ }
+ }
+ changed, err := changedFiles(mark.run.env, changes)
+ if err != nil {
+ mark.errorf("changedFiles failed: %v", err)
+ return
+ }
+
+ switch {
+ case edit != "":
+ g := mark.getGolden(edit)
+ checkDiffs(mark, changed, g)
+ case result != "":
+ g := mark.getGolden(result)
+ // Check the file state.
+ checkChangedFiles(mark, changed, g)
+ case !wantErr.empty():
+ wantErr.checkErr(mark, err)
+ default:
+ panic("unreachable")
+ }
+}
+
+func exactlyOneNamedArg(mark marker, names ...string) bool {
+ var found []string
+ for _, name := range names {
+ if _, ok := mark.note.NamedArgs[name]; ok {
+ found = append(found, name)
+ }
+ }
+ if len(found) != 1 {
+ mark.errorf("need exactly one of %v to be set, got %v", names, found)
+ return false
+ }
+ return true
+}
+
+// not used for @codeaction, but codeactions
+
+// codeAction executes a textDocument/codeAction request for the specified
+// location and kind. If diag is non-nil, it is used as the code action
+// context.
+//
+// The resulting map contains resulting file contents after the code action is
+// applied. Currently, this function does not support code actions that return
+// edits directly; it only supports code action commands.
+func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, kind protocol.CodeActionKind, diag *protocol.Diagnostic) (map[string][]byte, error) {
+ action, err := resolveCodeAction(env, uri, rng, kind, diag)
+ if err != nil {
+ return nil, err
+ }
+ changes, err := applyCodeAction(env, action)
+ if err != nil {
+ return nil, err
+ }
+ return changedFiles(env, changes)
+}
+
+// resolveCodeAction resolves the code action specified by the given location,
+// kind, and diagnostic.
+func resolveCodeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, kind protocol.CodeActionKind, diag *protocol.Diagnostic) (*protocol.CodeAction, error) {
+ // Request all code actions that apply to the diagnostic.
+ // A production client would set Only=[kind],
+ // but we can give a better error if we don't filter.
+ params := &protocol.CodeActionParams{
+ TextDocument: protocol.TextDocumentIdentifier{URI: uri},
+ Range: rng,
+ Context: protocol.CodeActionContext{
+ Only: []protocol.CodeActionKind{protocol.Empty}, // => all
+ //TriggerKind: protocol.CodeActionTriggerKind(1 /*Invoked*/),
+ },
+ }
+ if diag != nil {
+ params.Context.Diagnostics = []protocol.Diagnostic{*diag}
+ }
+
+ actions, err := env.Editor.Server.CodeAction(env.Ctx, params)
+ if err != nil {
+ return nil, err
+ }
+
+ // Find the sole candidate CodeAction of exactly the specified kind
+ // (e.g. refactor.inline.call).
+ var candidates []protocol.CodeAction
+ for _, act := range actions {
+ if act.Kind == kind {
+ candidates = append(candidates, act)
+ }
+ }
+ if len(candidates) != 1 {
+ var msg bytes.Buffer
+ fmt.Fprintf(&msg, "found %d CodeActions of kind %s for this diagnostic, want 1", len(candidates), kind)
+ for _, act := range actions {
+ fmt.Fprintf(&msg, "\n\tfound %q (%s)", act.Title, act.Kind)
+ }
+ return nil, errors.New(msg.String())
+ }
+ action := candidates[0]
+
+ // Resolve code action edits first if the client has resolve support
+ // and the code action has no edits.
+ if action.Edit == nil {
+ editSupport, err := env.Editor.EditResolveSupport()
+ if err != nil {
+ return nil, err
+ }
+ if editSupport {
+ resolved, err := env.Editor.Server.ResolveCodeAction(env.Ctx, &action)
+ if err != nil {
+ return nil, err
+ }
+ action = *resolved
+ }
+ }
+
+ return &action, nil
+}
+
+// applyCodeAction applies the given code action, and captures the resulting
+// document changes.
+func applyCodeAction(env *integration.Env, action *protocol.CodeAction) ([]protocol.DocumentChange, error) {
+ // PJW: this doesn't work for dialogs! Fix it!
+ // Collect any server-initiated changes created by workspace/applyEdit.
+ //
+ // We set up this handler immediately, not right before executing the code
+ // action command, so we can assert that neither the codeAction request nor
+ // codeAction resolve request cause edits as a side effect (golang/go#71405).
+ var changes []protocol.DocumentChange
+ restore := env.Editor.Client().SetApplyEditHandler(func(ctx context.Context, wsedit *protocol.WorkspaceEdit) error {
+ changes = append(changes, wsedit.DocumentChanges...)
+ return nil
+ })
+ defer restore()
+
+ if action.Edit == nil && action.Command == nil {
+ panic("bad action")
+ }
+
+ // Apply the codeAction.
+ //
+ // Spec:
+ // "If a code action provides an edit and a command, first the edit is
+ // executed and then the command."
+ // An action may specify an edit and/or a command, to be
+ // applied in that order. But since applyDocumentChanges(env,
+ // action.Edit.DocumentChanges) doesn't compose, for now we
+ // assert that actions return one or the other.
+ if action.Edit != nil {
+ if len(action.Edit.Changes) > 0 {
+ env.TB.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Edit.Changes", action.Kind, action.Title)
+ }
+ if action.Edit.DocumentChanges != nil {
+ if action.Command != nil {
+ env.TB.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Command", action.Kind, action.Title)
+ }
+ return action.Edit.DocumentChanges, nil
+ }
+ }
+
+ if action.Command != nil {
+ // This is a typical CodeAction command:
+ //
+ // Title: "Implement error"
+ // Command: gopls.apply_fix
+ // Arguments: [{"Fix":"stub_methods","URI":".../a.go","Range":...}}]
+ //
+ // The client makes an ExecuteCommand RPC to the server,
+ // which dispatches it to the ApplyFix handler.
+ // ApplyFix dispatches to the "stub_methods" fixer (the meat).
+ // The server then makes an ApplyEdit RPC to the client,
+ // whose WorkspaceEditFunc hook temporarily gathers the edits
+ // instead of applying them.
+ // PJW: this is inadequate in the new world of Forms and CommandResolve)
+
+ if _, err := env.Editor.Server.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{
+ Command: action.Command.Command,
+ Arguments: action.Command.Arguments,
+ }); err != nil {
+ return nil, err
+ }
+ return changes, nil // populated as a side effect of ExecuteCommand
+ }
+
+ return nil, nil
+}
diff --git a/gopls/internal/test/marker/doc.go b/gopls/internal/test/marker/doc.go
index 0b7afa4..ee87234 100644
--- a/gopls/internal/test/marker/doc.go
+++ b/gopls/internal/test/marker/doc.go
@@ -144,13 +144,15 @@
completion candidate produced at the given location with provided label
results in the given golden state.
- - codeaction(start location, kind string, diag=regexp, end=location, action=golden, edit=golden, result=golden, err=stringMatcher)
+ - codeaction(start location, kind string, diag=regexp, end=location, action=golden, edit=golden, result=golden, err=stringMatcher, form0=string, form1=string)
Specifies a code action to request at the location, with given kind.
If diag is set, the code action must be associated with the given
diagnostic.
If end is set, the location is defined to be between start.Start and
end.End.
+ If form0 is set the codeaction is invoking a user dialog and the form
+ arguments are the user response.
Exactly one of action, edit, result, or err must be set:
If action is set, it is a golden reference to a JSON blob representing the
diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go
index d99dfeb..eaad054 100644
--- a/gopls/internal/test/marker/marker_test.go
+++ b/gopls/internal/test/marker/marker_test.go
@@ -11,7 +11,6 @@
"bytes"
"context"
"encoding/json"
- "errors"
"flag"
"fmt"
"go/token"
@@ -583,20 +582,6 @@
return dflt
}
-func exactlyOneNamedArg(mark marker, names ...string) bool {
- var found []string
- for _, name := range names {
- if _, ok := mark.note.NamedArgs[name]; ok {
- found = append(found, name)
- }
- }
- if len(found) != 1 {
- mark.errorf("need exactly one of %v to be set, got %v", names, found)
- return false
- }
- return true
-}
-
// is reports whether arg is a T.
func is[T any](arg any) bool {
_, ok := arg.(T)
@@ -616,7 +601,7 @@
// See doc.go for marker documentation.
var actionMarkerFuncs = map[string]func(marker){
"acceptcompletion": actionMarkerFunc(acceptCompletionMarker),
- "codeaction": actionMarkerFunc(codeActionMarker, "end", "diag", "action", "result", "edit", "err"),
+ "codeaction": actionMarkerFunc(codeActionMarker, "end", "diag", "action", "result", "edit", "err", "form0", "form1"),
"codelenses": actionMarkerFunc(codeLensesMarker),
"complete": actionMarkerFunc(completeMarker),
"def": actionMarkerFunc(defMarker),
@@ -792,10 +777,23 @@
//
// See the documentation for RunMarkerTests for more details on the test data
// archive.
-func loadMarkerTests(dir string) ([]*markerTest, error) {
+func loadMarkerTests(dir string, only ...string) ([]*markerTest, error) {
var tests []*markerTest
+ var use *regexp.Regexp
+ if len(only) > 0 {
+ var err error
+ use, err = regexp.Compile(strings.Join(only, "|"))
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ use = regexp.MustCompile(".")
+ }
err := filepath.WalkDir(dir, func(path string, _ fs.DirEntry, err error) error {
if strings.HasSuffix(path, ".txt") {
+ if !use.MatchString(path) {
+ return nil
+ }
content, err := os.ReadFile(path)
if err != nil {
return err
@@ -1490,7 +1488,7 @@
}
// checkDiffs checks that the diff content stored in the given golden directory
-// converts the original contents into the changed contents.
+// converts the orginal contents into the changed contents.
// (This logic is strange because hundreds of existing marker tests were created
// containing a modified version of unified diffs.)
func checkDiffs(mark marker, changed map[string][]byte, golden *Golden) {
@@ -1518,7 +1516,7 @@
mark.note.Name, name, got)
return
} else {
- // restore the ToUnified header lines deleted above
+ // restore the ToUnifeed header lines deleted above
// before calling ApplyUnified
diffsFromTest := "--- \n+++ \n" + string(tdiffs)
want, err := diff.ApplyUnified(diffsFromTest, before)
@@ -2223,93 +2221,6 @@
return result, nil
}
-func codeActionMarker(mark marker, loc protocol.Location, kind string) {
- if !exactlyOneNamedArg(mark, "action", "edit", "result", "err") {
- return
- }
-
- if end := namedArgFunc(mark, "end", convertNamedArgLocation, protocol.Location{}); end.URI != "" {
- if end.URI != loc.URI {
- mark.errorf("end marker is in a different file (%s)", filepath.Base(loc.URI.Path()))
- return
- }
- loc.Range.End = end.Range.End
- }
-
- var (
- edit = namedArg(mark, "edit", expect.Identifier(""))
- result = namedArg(mark, "result", expect.Identifier(""))
- wantAction = namedArg(mark, "action", expect.Identifier(""))
- wantErr = namedArgFunc(mark, "err", convertStringMatcher, stringMatcher{})
- )
-
- var diag *protocol.Diagnostic
- if re := namedArg(mark, "diag", (*regexp.Regexp)(nil)); re != nil {
- d, ok := removeDiagnostic(mark, loc, false, re)
- if !ok {
- mark.errorf("no diagnostic at %v matches %q", loc, re)
- return
- }
- diag = &d
- }
-
- action, err := resolveCodeAction(mark.run.env, loc.URI, loc.Range, protocol.CodeActionKind(kind), diag)
- if err != nil {
- if !wantErr.empty() {
- wantErr.checkErr(mark, err)
- } else {
- mark.errorf("resolveCodeAction failed: %v", err)
- }
- return
- }
-
- // If when 'action' is set, we simply compare the action, and don't apply it.
- if wantAction != "" {
- g := mark.getGolden(wantAction)
- if action == nil {
- mark.errorf("no matching codeAction")
- return
- }
- data, err := json.MarshalIndent(action, "", "\t")
- if err != nil {
- mark.errorf("failed to marshal codeaction: %v", err)
- return
- }
- data = bytes.ReplaceAll(data, []byte(mark.run.env.Sandbox.Workdir.RootURI()), []byte("$WORKDIR"))
- compareGolden(mark, data, g)
- return
- }
-
- changes, err := applyCodeAction(mark.run.env, action)
- if err != nil {
- if !wantErr.empty() {
- wantErr.checkErr(mark, err)
- } else {
- mark.errorf("codeAction failed: %v", err)
- }
- return
- }
- changed, err := changedFiles(mark.run.env, changes)
- if err != nil {
- mark.errorf("changedFiles failed: %v", err)
- return
- }
-
- switch {
- case edit != "":
- g := mark.getGolden(edit)
- checkDiffs(mark, changed, g)
- case result != "":
- g := mark.getGolden(result)
- // Check the file state.
- checkChangedFiles(mark, changed, g)
- case !wantErr.empty():
- wantErr.checkErr(mark, err)
- default:
- panic("unreachable")
- }
-}
-
// codeLensesMarker runs the @codelenses() marker, collecting @codelens marks
// in the current file and comparing with the result of the
// textDocument/codeLens RPC.
@@ -2414,150 +2325,6 @@
wantErr.checkErr(mark, err)
}
-// codeAction executes a textDocument/codeAction request for the specified
-// location and kind. If diag is non-nil, it is used as the code action
-// context.
-//
-// The resulting map contains resulting file contents after the code action is
-// applied. Currently, this function does not support code actions that return
-// edits directly; it only supports code action commands.
-func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, kind protocol.CodeActionKind, diag *protocol.Diagnostic) (map[string][]byte, error) {
- action, err := resolveCodeAction(env, uri, rng, kind, diag)
- if err != nil {
- return nil, err
- }
- changes, err := applyCodeAction(env, action)
- if err != nil {
- return nil, err
- }
- return changedFiles(env, changes)
-}
-
-// resolveCodeAction resolves the code action specified by the given location,
-// kind, and diagnostic.
-func resolveCodeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, kind protocol.CodeActionKind, diag *protocol.Diagnostic) (*protocol.CodeAction, error) {
- // Request all code actions that apply to the diagnostic.
- // A production client would set Only=[kind],
- // but we can give a better error if we don't filter.
- params := &protocol.CodeActionParams{
- TextDocument: protocol.TextDocumentIdentifier{URI: uri},
- Range: rng,
- Context: protocol.CodeActionContext{
- Only: []protocol.CodeActionKind{protocol.Empty}, // => all
- },
- }
- if diag != nil {
- params.Context.Diagnostics = []protocol.Diagnostic{*diag}
- }
-
- actions, err := env.Editor.Server.CodeAction(env.Ctx, params)
- if err != nil {
- return nil, err
- }
-
- // Find the sole candidate CodeAction of exactly the specified kind
- // (e.g. refactor.inline.call).
- var candidates []protocol.CodeAction
- for _, act := range actions {
- if act.Kind == kind {
- candidates = append(candidates, act)
- }
- }
- if len(candidates) != 1 {
- var msg bytes.Buffer
- fmt.Fprintf(&msg, "found %d CodeActions of kind %s for this diagnostic, want 1", len(candidates), kind)
- for _, act := range actions {
- fmt.Fprintf(&msg, "\n\tfound %q (%s)", act.Title, act.Kind)
- }
- return nil, errors.New(msg.String())
- }
- action := candidates[0]
-
- // Resolve code action edits first if the client has resolve support
- // and the code action has no edits.
- if action.Edit == nil {
- editSupport, err := env.Editor.EditResolveSupport()
- if err != nil {
- return nil, err
- }
- if editSupport {
- resolved, err := env.Editor.Server.ResolveCodeAction(env.Ctx, &action)
- if err != nil {
- return nil, err
- }
- action = *resolved
- }
- }
-
- return &action, nil
-}
-
-// applyCodeAction applies the given code action, and captures the resulting
-// document changes.
-func applyCodeAction(env *integration.Env, action *protocol.CodeAction) ([]protocol.DocumentChange, error) {
- // Collect any server-initiated changes created by workspace/applyEdit.
- //
- // We set up this handler immediately, not right before executing the code
- // action command, so we can assert that neither the codeAction request nor
- // codeAction resolve request cause edits as a side effect (golang/go#71405).
- var changes []protocol.DocumentChange
- restore := env.Editor.Client().SetApplyEditHandler(func(ctx context.Context, wsedit *protocol.WorkspaceEdit) error {
- changes = append(changes, wsedit.DocumentChanges...)
- return nil
- })
- defer restore()
-
- if action.Edit == nil && action.Command == nil {
- panic("bad action")
- }
-
- // Apply the codeAction.
- //
- // Spec:
- // "If a code action provides an edit and a command, first the edit is
- // executed and then the command."
- // An action may specify an edit and/or a command, to be
- // applied in that order. But since applyDocumentChanges(env,
- // action.Edit.DocumentChanges) doesn't compose, for now we
- // assert that actions return one or the other.
- if action.Edit != nil {
- if len(action.Edit.Changes) > 0 {
- env.TB.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Edit.Changes", action.Kind, action.Title)
- }
- if action.Edit.DocumentChanges != nil {
- if action.Command != nil {
- env.TB.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Command", action.Kind, action.Title)
- }
- return action.Edit.DocumentChanges, nil
- }
- }
-
- if action.Command != nil {
- // This is a typical CodeAction command:
- //
- // Title: "Implement error"
- // Command: gopls.apply_fix
- // Arguments: [{"Fix":"stub_methods","URI":".../a.go","Range":...}}]
- //
- // The client makes an ExecuteCommand RPC to the server,
- // which dispatches it to the ApplyFix handler.
- // ApplyFix dispatches to the "stub_methods" fixer (the meat).
- // The server then makes an ApplyEdit RPC to the client,
- // whose WorkspaceEditFunc hook temporarily gathers the edits
- // instead of applying them.
-
- if _, err := env.Editor.Server.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{
- Command: action.Command.Command,
- Arguments: action.Command.Arguments,
- }); err != nil {
- return nil, err
- }
- return changes, nil // populated as a side effect of ExecuteCommand
- }
-
- return nil, nil
-}
-
// refsMarker implements the @refs marker.
func refsMarker(mark marker, src protocol.Location, want ...protocol.Location) {
refs := func(includeDeclaration bool, want []protocol.Location) error {
diff --git a/gopls/internal/test/marker/testdata/codeaction/dialog_add_tags.txt b/gopls/internal/test/marker/testdata/codeaction/dialog_add_tags.txt
new file mode 100644
index 0000000..6c8e754
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/codeaction/dialog_add_tags.txt
@@ -0,0 +1,37 @@
+Test struct tags with dialogs
+
+-- capabilities.json --
+{
+ "experimental":{"interactiveInputTypes":["string","bool","number","enum"]}
+}
+
+-- flags --
+-ignore_extra_diags
+
+
+-- addtags.go --
+package addtags
+
+type A struct {
+ x int //@codeaction("x", "refactor.rewrite.addTags", edit=singleline, form0="xml", form1="camelcase")
+ y int //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.addTags", edit=twolines, form0="xml", form1="camelcase")
+ z int //@codeaction(re`()n`, "refactor.rewrite.addTags", edit=entirestruct, form0="xml", form1="camelcase")
+}
+-- @entirestruct/addtags.go --
+@@ -4,3 +4,3 @@
+- x int //@codeaction("x", "refactor.rewrite.addTags", edit=singleline, form0="xml", form1="camelcase")
+- y int //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.addTags", edit=twolines, form0="xml", form1="camelcase")
+- z int //@codeaction(re`()n`, "refactor.rewrite.addTags", edit=entirestruct, form0="xml", form1="camelcase")
++ x int `xml:"x"` //@codeaction("x", "refactor.rewrite.addTags", edit=singleline, form0="xml", form1="camelcase")
++ y int `xml:"y"` //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.addTags", edit=twolines, form0="xml", form1="camelcase")
++ z int `xml:"z"` //@codeaction(re`()n`, "refactor.rewrite.addTags", edit=entirestruct, form0="xml", form1="camelcase")
+-- @singleline/addtags.go --
+@@ -4 +4 @@
+- x int //@codeaction("x", "refactor.rewrite.addTags", edit=singleline, form0="xml", form1="camelcase")
++ x int `xml:"x"` //@codeaction("x", "refactor.rewrite.addTags", edit=singleline, form0="xml", form1="camelcase")
+-- @twolines/addtags.go --
+@@ -5,2 +5,2 @@
+- y int //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.addTags", edit=twolines, form0="xml", form1="camelcase")
+- z int //@codeaction(re`()n`, "refactor.rewrite.addTags", edit=entirestruct, form0="xml", form1="camelcase")
++ y int `xml:"y"` //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.addTags", edit=twolines, form0="xml", form1="camelcase")
++ z int `xml:"z"` //@codeaction(re`()n`, "refactor.rewrite.addTags", edit=entirestruct, form0="xml", form1="camelcase")
diff --git a/gopls/internal/test/marker/testdata/codeaction/dialog_remove_tags.txt b/gopls/internal/test/marker/testdata/codeaction/dialog_remove_tags.txt
new file mode 100644
index 0000000..cfdc4a7
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/codeaction/dialog_remove_tags.txt
@@ -0,0 +1,38 @@
+Test struct tags with dialogs
+
+(why can't we also test addtags in this file?)
+
+-- capabilities.json --
+{
+ "experimental":{"interactiveInputTypes":["string","bool","number","enum"]}
+}
+
+-- flags --
+-ignore_extra_diags
+
+-- removetags.go --
+package removetags
+
+type A struct {
+ x int `xml:"x"` //@codeaction("x", "refactor.rewrite.removeTags", edit=singleline, form0="xml")
+ y int `xml:"y"` //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.removeTags", edit=twolines, form0="xml")
+ z int `xml:"z"` //@codeaction(re`()n`, "refactor.rewrite.removeTags", edit=entirestruct, form0="xml")
+}
+-- @entirestruct/removetags.go --
+@@ -4,3 +4,3 @@
+- x int `xml:"x"` //@codeaction("x", "refactor.rewrite.removeTags", edit=singleline, form0="xml")
+- y int `xml:"y"` //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.removeTags", edit=twolines, form0="xml")
+- z int `xml:"z"` //@codeaction(re`()n`, "refactor.rewrite.removeTags", edit=entirestruct, form0="xml")
++ x int //@codeaction("x", "refactor.rewrite.removeTags", edit=singleline, form0="xml")
++ y int //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.removeTags", edit=twolines, form0="xml")
++ z int //@codeaction(re`()n`, "refactor.rewrite.removeTags", edit=entirestruct, form0="xml")
+-- @singleline/removetags.go --
+@@ -4 +4 @@
+- x int `xml:"x"` //@codeaction("x", "refactor.rewrite.removeTags", edit=singleline, form0="xml")
++ x int //@codeaction("x", "refactor.rewrite.removeTags", edit=singleline, form0="xml")
+-- @twolines/removetags.go --
+@@ -5,2 +5,2 @@
+- y int `xml:"y"` //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.removeTags", edit=twolines, form0="xml")
+- z int `xml:"z"` //@codeaction(re`()n`, "refactor.rewrite.removeTags", edit=entirestruct, form0="xml")
++ y int //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.removeTags", edit=twolines, form0="xml")
++ z int //@codeaction(re`()n`, "refactor.rewrite.removeTags", edit=entirestruct, form0="xml")
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
var modifytags command.ModifyTagsArgsHi Peter, I'm confused by the fact that we have some server awared code in the client.
I thought we could just have the client read the output of "command/resolve" and pass the returned ExecuteCommandParam to the `workspace/executeCommand` below.
got, err = mark.run.env.Editor.Server.ExecuteCommand(mark.run.env.Ctx, cmd)What is the "got" meant to be used here. I'm not very familiar with the marker test. I thought the "got" above at line 121 is already obtained and we just need to compare the "got" with the "want" specified from the test file `@singline/addtest.go`...
// PJW: this is inadequate in the new world of Forms and CommandResolve)I have not fully understand the purpose of the action=? param in the code action. I know this is not your change...
can we just pass the ExecuteCommandParams to the command/resolve and pass the form 0 and form 1 and then call command/resolve, and then execute the command?
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Hi Peter, I'm confused by the fact that we have some server awared code in the client.
I thought we could just have the client read the output of "command/resolve" and pass the returned ExecuteCommandParam to the `workspace/executeCommand` below.
I added a comment saying that this is the gopls version of interface GoModifyTagsArgs in goModifyTags.ts in vscode-go.
got, err = mark.run.env.Editor.Server.ExecuteCommand(mark.run.env.Ctx, cmd)What is the "got" meant to be used here. I'm not very familiar with the marker test. I thought the "got" above at line 121 is already obtained and we just need to compare the "got" with the "want" specified from the test file `@singline/addtest.go`...
I change it to _, as we don't look at the value (which is nil)
// PJW: this is inadequate in the new world of Forms and CommandResolve)I have not fully understand the purpose of the action=? param in the code action. I know this is not your change...
can we just pass the ExecuteCommandParams to the command/resolve and pass the form 0 and form 1 and then call command/resolve, and then execute the command?
See line 157 in markertest/doc.go for the action= parameter. (I don't understand them either). I've reorganized the code with a new function applyCodeActionForm().
And I think we could call ResolveCommand() directly, but this code simulates the interaction with vscode-go, which seemed to me to be more stressful.
Let me know if you want me to change it.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
// wants a dialogI assume this belongs to
```
if namedArg(mark, "form0", "") != "" { // wants a dialog
```
naction, err := mark.run.env.Editor.Server.ResolveCodeAction(mark.run.env.Ctx, action)
if err != nil {
mark.errorf("ResolveCodeAction failed: %v", err)
return changes, err
}the codeAction/resolve is already called in line 53, so I don't think there is a need call this again.
var modifytags command.ModifyTagsArgs // Gopls version of GoModifyTagsArgs in goModifytags.ts in vscode-goHi peter, I think `goModifyTags.ts` is not the correct place to look.
GoModifyTags.ts is not the dialog implementation in vscode-go. `form.ts` is the right place, the function name is `ResolveCommand`.
The logic of the fake client should looks like below
```
// The client sends three workspace/executeCommand to the server.
// The first (underneath is command/resolve) is to get the form for the dialog
// The second (underneath is command/resolve) is to return the user's choices
// The thid (underneath is the target command) is to run the command and
// apply edits.
original := protocol.ExecuteCommandParams{
Command: action.Command.Command,
Arguments: action.Command.Arguments,
}
withQuestions := new(protocol.ExecuteCommandParams)
{
// Call workspace/executeCommand whose underneath is gopls.lsp
// command/resolve.
....
// The return should be a [protocol.ExecuteCommandParams] with
// question.
if err := json.Unmarshal(???, withQuestions); err != nil {
log.Printf("marshaled %s", string(marshaled))
return changes, err
}
}
// Simulate the langauge client collect input from the user and insert
// the answer to the [protocol.ExecuteCommandParam.FormAnswers].
//
// Remove the questions and attach the answers.
withQuestions.FormFields = nil
if mark.note.NamedArgs["form0"] != nil {
withQuestions.FormAnswers = append(withQuestions.FormAnswers, mark.note.NamedArgs["form0"])
}
if mark.note.NamedArgs["form1"] != nil {
withQuestions.FormAnswers = append(withQuestions.FormAnswers, mark.note.NamedArgs["form1"])
}
// Second workspace/executeCommand (whose underneath is gopls.lsp) receive
// the execute command with answers and return execute command.
final := new(protocol.ExecuteCommandParams)
{
// Call workspace/executeCommand whose underneath is gopls.lsp
// command/resolve. The input is param with answers.
....
// The return should be a [protocol.ExecuteCommandParams] without
// any questions. Because the gopls read the answer and embed the
// answers to the command.args.
// Validate the return should not have any questions.
if err := json.Unmarshal(???, final); err != nil {
log.Printf("marshaled %s", string(marshaled))
return changes, err
}
}
// catch the changes from the forthcoming ExecuteCommand
restore := mark.run.env.Editor.Client().SetApplyEditHandler(func(ctx context.Context, wsedit *protocol.WorkspaceEdit) error {
changes = append(changes, wsedit.DocumentChanges...)
return nil
})
defer restore()
// Third workspace/executeCommand (whose underneath is the target command
// e.g. gopls.modifyTags ...) runs the target command.
// expect nil return. Side effect is to change the file
_, err := mark.run.env.Editor.Server.ExecuteCommand(mark.run.env.Ctx, final)
if err != nil {
mark.errorf("ExecuteCommand failed: %v", err)
return changes, err
}
return changes, nil
```
switch kind {
case "refactor.rewrite.removeTags":
modifytags.Remove = mark.note.NamedArgs["form0"].(string)
case "refactor.rewrite.addTags":
modifytags.Add = mark.note.NamedArgs["form0"].(string)
modifytags.Transform = mark.note.NamedArgs["form1"].(string)
default:
mark.errorf("unexpected kind %q", kind)
return changes, err
}We should not have any code that aware of how the langauge server works, like the type of "ModifyTags".
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
tests, err := loadMarkerTests(dir, "dialog_")I think this is not intentional, right?
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
mark.errorf("ExecuteCommand failed: %v", err)the code action marker allow us to verify the error `@codeaction(... err=re'..')`
the error can be a code action resolve error, or it can be a execute command error.
instead of fail the test here, we need to let the caller at line 86 to verify if the error we encountered what expected or not.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |