gopls/internal/golang: add Move Declaration skeleton Add code action and command for Move Declaration feature, along with an off-by-default setting to gate its use. Use interactive command to get the destination file. I will delete the MoveType code action in a follow-up CL, I thought it would be easier to review a clean addition versus renaming everything in MoveType. Change-Id: I2d8bafd29c376ba00a776c5ac441323f63e751b6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/800940 Reviewed-by: Alan Donovan <adonovan@google.com> Auto-Submit: Madeline Kalil <mkalil@google.com> Reviewed-by: Alex Putman <aputman@golang.org> Reviewed-by: Hongxiang Jiang <hxjiang@golang.org> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 8e0602c..63415d9 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md
@@ -301,6 +301,16 @@ Default: `false`. +<a id='moveDeclaration'></a> +### `moveDeclaration bool` + +**This setting is experimental and may be deleted.** + +moveDeclaration enables producing Move Declaration codeactions. The implementation +is unfinished so we use this setting to gate its use. + +Default: `false`. + <a id='completion'></a> ## Completion
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index 675a5c4..4596959 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json
@@ -2289,6 +2289,20 @@ "DeprecationMessage": "" }, { + "Name": "moveDeclaration", + "Type": "bool", + "Doc": "moveDeclaration enables producing Move Declaration codeactions. The implementation\nis unfinished so we use this setting to gate its use.\n", + "EnumKeys": { + "ValueType": "", + "Keys": null + }, + "EnumValues": null, + "Default": "false", + "Status": "experimental", + "Hierarchy": "ui", + "DeprecationMessage": "" + }, + { "Name": "local", "Type": "string", "Doc": "local is the equivalent of the `goimports -local` flag, which puts\nimports beginning with this string after third-party packages. It should\nbe the prefix of the import path whose imports should be grouped\nseparately.\n\nIt is used when tidying imports (during an LSP Organize\nImports request) or when inserting new ones (for example,\nduring completion); an LSP Formatting request merely sorts the\nexisting imports.\n",
diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 9ae120a..14465d4 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go
@@ -257,6 +257,7 @@ {kind: settings.RefactorInlineCall, fn: refactorInlineCall, needPkg: true}, {kind: settings.RefactorInlineVariable, fn: refactorInlineVariable, needPkg: true}, {kind: settings.RefactorMoveType, fn: refactorMoveType, needPkg: true}, + {kind: settings.RefactorMoveDeclaration, fn: refactorMoveDeclaration, needPkg: true}, {kind: settings.RefactorRewriteChangeQuote, fn: refactorRewriteChangeQuote}, {kind: settings.RefactorRewriteFillStruct, fn: refactorRewriteFillStruct, needPkg: true}, {kind: settings.RefactorRewriteFillSwitch, fn: refactorRewriteFillSwitch, needPkg: true}, @@ -1259,3 +1260,16 @@ } return nil } + +func refactorMoveDeclaration(_ context.Context, req *codeActionsRequest) error { + if !req.snapshot.Options().MoveDeclaration { + return nil + } + if !supportsDialog(req.snapshot.Options().ClientOptions, moveDeclarationFormFile, moveDeclarationFormString) { + return nil + } + curSel, _ := req.pgf.Cursor().FindByPos(req.start, req.end) + cmd := command.NewMoveDeclarationCommand(fmt.Sprintf("Move declaration %s", curSel.Node()), command.MoveDeclarationArgs{Location: req.loc}) + req.addCommandAction(cmd, false) + return nil +}
diff --git a/gopls/internal/golang/movedeclaration.go b/gopls/internal/golang/movedeclaration.go new file mode 100644 index 0000000..85808ce --- /dev/null +++ b/gopls/internal/golang/movedeclaration.go
@@ -0,0 +1,17 @@ +// 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 golang + +import ( + "context" + + "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/protocol" +) + +func MoveDeclaration(ctx context.Context, fh file.Handle, snapshot *cache.Snapshot) ([]protocol.DocumentChange, protocol.Location, error) { + return nil, protocol.Location{}, nil +}
diff --git a/gopls/internal/golang/resolve.go b/gopls/internal/golang/resolve.go index c4ad48c..4c59d0a 100644 --- a/gopls/internal/golang/resolve.go +++ b/gopls/internal/golang/resolve.go
@@ -115,6 +115,10 @@ if err := resolveImplementInterface(options, params); err != nil { return nil, err } + case "gopls.move_declaration": + if err := resolveMoveDeclaration(options, params); err != nil { + return nil, err + } } return params, nil } @@ -221,6 +225,28 @@ }, } +var moveDeclarationFormString = []protocol.FormField{ + { + ID: "string", + Description: "destination file uri for the moved declaration, e.g. file:///path/to/file.go", + Type: protocol.FormFieldTypeFile{ + Kind: "string", + }, + Required: true, + }, +} + +var moveDeclarationFormFile = []protocol.FormField{ + { + ID: "file", + Description: "destination file for the moved declaration", + Type: protocol.FormFieldTypeFile{ + Kind: "file", + }, + Required: true, + }, +} + func resolveImplementInterface(options settings.ClientOptions, param *protocol.ExecuteCommandParams) error { var a0 command.ImplementInterfaceArgs if err := command.UnmarshalArgs(param.Arguments, &a0); err != nil { @@ -285,6 +311,39 @@ return nil } +func resolveMoveDeclaration(options settings.ClientOptions, param *protocol.ExecuteCommandParams) error { + var a0 command.MoveDeclarationArgs + if err := command.UnmarshalArgs(param.Arguments, &a0); err != nil { + return err + } + var form []protocol.FormField + if ok := options.SupportedInteractiveInputTypes[settings.InteractiveInputTypeFile]; ok { + form = moveDeclarationFormFile + } else if ok := options.SupportedInteractiveInputTypes[settings.InteractiveInputTypeString]; ok { + form = moveDeclarationFormString + } else { + // This should not happen because gopls should not offer this code action if the + // language client does not support any kind above. + return fmt.Errorf("internal error: unsupported interactive input types: %v", options.SupportedInteractiveInputTypes) + } + + // First call, return the empty form. + if len(param.FormAnswers) == 0 { + param.FormFields = form + return nil + } + + file, err := FormAnswer[string](¶m.InteractiveParams, "file") + if err != nil { + return err + } + if _, err := protocol.ParseDocumentURI(file); err != nil { + return err + } + param.FormFields = nil + return nil +} + // FormAnswer finds, validates, and returns the unique answer for id. // // It uses a linear scan since the number of answers is small (usually < 5).
diff --git a/gopls/internal/protocol/command/command_gen.go b/gopls/internal/protocol/command/command_gen.go index 7d8c47c..16b5a40 100644 --- a/gopls/internal/protocol/command/command_gen.go +++ b/gopls/internal/protocol/command/command_gen.go
@@ -50,6 +50,7 @@ MemStats Command = "gopls.mem_stats" ModifyTags Command = "gopls.modify_tags" Modules Command = "gopls.modules" + MoveDeclaration Command = "gopls.move_declaration" MoveType Command = "gopls.move_type" PackageSymbols Command = "gopls.package_symbols" Packages Command = "gopls.packages" @@ -100,6 +101,7 @@ MemStats, ModifyTags, Modules, + MoveDeclaration, MoveType, PackageSymbols, Packages, @@ -276,6 +278,12 @@ return nil, err } return s.Modules(ctx, a0) + case MoveDeclaration: + var a0 MoveDeclarationArgs + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + return nil, s.MoveDeclaration(ctx, a0, ¶ms.InteractiveParams) case MoveType: var a0 MoveTypeArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -603,6 +611,14 @@ } } +func NewMoveDeclarationCommand(title string, a0 MoveDeclarationArgs) *protocol.Command { + return &protocol.Command{ + Title: title, + Command: MoveDeclaration.String(), + Arguments: MustMarshalArgs(a0), + } +} + func NewMoveTypeCommand(title string, a0 MoveTypeArgs) *protocol.Command { return &protocol.Command{ Title: title,
diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go index d1757ae..1f94728 100644 --- a/gopls/internal/protocol/command/interface.go +++ b/gopls/internal/protocol/command/interface.go
@@ -336,6 +336,9 @@ // ImplementInterface: Add methods to a type to implement an interface. ImplementInterface(context.Context, ImplementInterfaceArgs, *protocol.InteractiveParams) error + + // MoveDeclaration: Move a declaration to a different file. + MoveDeclaration(context.Context, MoveDeclarationArgs, *protocol.InteractiveParams) error } type RunTestsArgs struct { @@ -914,3 +917,9 @@ // TODO(mkalil): Determine format of the parameter that specifies where to // move the type to. } + +// MoveDeclarationArgs specifies a "move declaration" refactoring to perform. +type MoveDeclarationArgs struct { + // The location of the declaration to move. + Location protocol.Location +}
diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index d858f17..94fe90c 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go
@@ -1954,3 +1954,15 @@ }) return err } + +func (c *commandHandler) MoveDeclaration(ctx context.Context, args command.MoveDeclarationArgs, params *protocol.InteractiveParams) error { + return c.run(ctx, commandConfig{ + forURI: args.Location.URI, + }, func(ctx context.Context, deps commandDeps) error { + changes, _, err := golang.MoveDeclaration(ctx, deps.fh, deps.snapshot) + if err != nil { + return err + } + return applyChanges(ctx, c.s.client, changes) + }) +}
diff --git a/gopls/internal/settings/codeactionkind.go b/gopls/internal/settings/codeactionkind.go index dea15f9..ba62211 100644 --- a/gopls/internal/settings/codeactionkind.go +++ b/gopls/internal/settings/codeactionkind.go
@@ -116,7 +116,8 @@ RefactorExtractToNewFile protocol.CodeActionKind = "refactor.extract.toNewFile" // refactor.move - RefactorMoveType protocol.CodeActionKind = "refactor.move.moveType" + RefactorMoveType protocol.CodeActionKind = "refactor.move.moveType" + RefactorMoveDeclaration protocol.CodeActionKind = "refactor.move.moveDeclaration" // Note: add new kinds to: // - the SupportedCodeActions map in default.go
diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go index fa9e44f..0086d5f 100644 --- a/gopls/internal/settings/default.go +++ b/gopls/internal/settings/default.go
@@ -73,6 +73,7 @@ RefactorExtractVariableAll: true, RefactorExtractToNewFile: true, RefactorMoveType: true, // gated by MoveType setting, which is off by default + RefactorMoveDeclaration: true, // gated by MoveDeclaration setting, which is off by default // Not GoTest: it must be explicit in CodeActionParams.Context.Only }, file.Mod: {
diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index 5083297..987223a 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go
@@ -274,6 +274,10 @@ // MoveType enables producing Move Type codeactions. The implementation // is unfinished so we use this setting to gate its use. MoveType bool `status:"experimental"` + + // MoveDeclaration enables producing Move Declaration codeactions. The implementation + // is unfinished so we use this setting to gate its use. + MoveDeclaration bool `status:"experimental"` } // A CodeLensSource identifies an (algorithmic) source of code lenses. @@ -1478,6 +1482,9 @@ case "moveType": return setBool(&o.MoveType, value) + case "moveDeclaration": + return setBool(&o.MoveDeclaration, value) + // deprecated and renamed settings // // These should never be deleted: there is essentially no cost