gopls/internal/golang: MoveDeclaration - move doc comments and trailing comments

Expand the movingRange of a moving decl to include the start of its
doc comment and the end of its trailing comment (if any).

Copying the doc comment text without adjusting indent results
in weird indentation, but gofmt will fix this so it is left
unchanged (but added a TODO).

Also adjust the end range of moving decls that will
be deleted to include trailing whitespace so we don't
leave a bunch of blank lines in the source file.

For golang/go#70583

Change-Id: If718c2e85ead673687fba532c7b8c81f199b2966
Reviewed-on: https://go-review.googlesource.com/c/tools/+/846685
Reviewed-by: Hongxiang Jiang <hxjiang@golang.org>
Auto-Submit: 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/internal/golang/move_decl.go b/gopls/internal/golang/move_decl.go
index cad473a..0090d84 100644
--- a/gopls/internal/golang/move_decl.go
+++ b/gopls/internal/golang/move_decl.go
@@ -15,6 +15,7 @@
 	"os"
 	"path/filepath"
 	"slices"
+	"sort"
 	"strconv"
 	"strings"
 
@@ -188,7 +189,18 @@
 	// Source: remove the moving decls. Moving decls may be in any file of srcPkg,
 	// not just srcPGF.
 	for _, r := range movingRanges {
-		rng, err := r.pgf.PosRange(r.start, r.end)
+		// Also delete trailing empty lines, to avoid leaving blocks of whitespace
+		// in the src files.
+		// TODO(mkalil): address the skewed indentation when removing single specs
+		// from a GenDecl.
+		end := r.end
+		offset, err := safetoken.Offset(r.pgf.Tok, end)
+		if err != nil {
+			return nil, protocol.Location{}, err
+		}
+		rest := r.pgf.Src[offset:]
+		end += token.Pos(len(rest) - len(bytes.TrimLeft(rest, " \t\n")))
+		rng, err := r.pgf.PosRange(r.start, end)
 		if err != nil {
 			return nil, protocol.Location{}, err
 		}
@@ -685,15 +697,16 @@
 	start, end token.Pos
 	// Keyword (e.g. var, const, type) if the range is a single spec of a GenDecl;
 	// token.ILLEGAL if it is a whole decl.
-	tok token.Token
+	tok  token.Token
+	node ast.Node // the moving *ast.FuncDecl, *ast.GenDecl, or ast.Spec
 }
 
 // Range returns the span of r in its source file.
 func (r movingRange) Range() astutil.Range { return astutil.RangeOf(r.start, r.end) }
 
-// text returns the source text of the movingRange r to insert in the
-// destination file. If r is a spec, its keyword is prepended to make it a
-// declaration, e.g. "var" in "var x = 1".
+// text returns the source text of the movingRange r, including its doc comment
+// and any trailing comments, to insert in the destination file. If r is a spec,
+// its keyword is prepended to make it a declaration, e.g. "var" in "var x = 1".
 // References to destPkg within r lose their qualifier, e.g. "dest.F()" becomes "F()".
 func (r movingRange) text(info *types.Info, destPkgPath string) (string, error) {
 	startOff, endOff, err := safetoken.Offsets(r.pgf.Tok, r.start, r.end)
@@ -703,8 +716,8 @@
 	// Delete the qualifier of each reference to destPkg.
 	// Edit offsets are relative to startOff.
 	var edits []diff.Edit
-	if curRange, ok := r.pgf.Cursor().FindByPos(r.start, r.end); ok {
-		for curSel := range curRange.Preorder((*ast.SelectorExpr)(nil)) {
+	if curNode, ok := r.pgf.Cursor().FindNode(r.node); ok {
+		for curSel := range curNode.Preorder((*ast.SelectorExpr)(nil)) {
 			sel := curSel.Node().(*ast.SelectorExpr)
 			if id, ok := sel.X.(*ast.Ident); ok {
 				if pkgName, ok := info.Uses[id].(*types.PkgName); ok && pkgName.Imported().Path() == destPkgPath {
@@ -723,8 +736,14 @@
 		return "", err
 	}
 	if r.tok != token.ILLEGAL {
-		// Prepend a keyword ("var" or "type") to a single spec.
-		return r.tok.String() + " " + text, nil
+		// Prepend a keyword ("var", "const", or "type") to a single spec, after its
+		// doc comment, if any.
+		nodeOff, err := safetoken.Offset(r.pgf.Tok, r.node.Pos())
+		if err != nil {
+			return "", err
+		}
+		docCommentLen := nodeOff - startOff
+		return text[:docCommentLen] + r.tok.String() + " " + text[docCommentLen:], nil
 	}
 	return text, nil
 }
@@ -741,8 +760,6 @@
 // extractMovingDeclarations returns the declarations to move, in file order.
 // If all specs of a GenDecl are moving, its range covers the whole GenDecl;
 // otherwise each moving spec gets its own range.
-// TODO(mkalil): Preserve doc comments, trailing comments, and comments between
-// adjacent decls that are both moving.
 func extractMovingDeclarations(srcPkg *cache.Package, moving map[types.Object]bool) ([]movingRange, error) {
 	var (
 		info         = srcPkg.TypesInfo()
@@ -754,7 +771,22 @@
 	}
 	for _, pgf := range srcPkg.CompiledGoFiles() {
 		addMovingRange := func(n ast.Node, tok token.Token) {
-			movingRanges = append(movingRanges, movingRange{pgf: pgf, start: n.Pos(), end: n.End(), tok: tok})
+			// Include the decl's doc comment.
+			// TODO(hxjiang): consolidate this logic with the logic in hover.go that
+			// finds the comments associated with a node.
+			start := n.Pos()
+			if doc := astutil.DocComment(n); doc != nil {
+				start = doc.Pos()
+			}
+			// Include a trailing comment on the node's last line.
+			end := n.End()
+			comments := pgf.File.Comments
+			// Find the first comment that is located after the end of the node.
+			i := sort.Search(len(comments), func(i int) bool { return comments[i].Pos() >= end })
+			if i < len(comments) && safetoken.Line(pgf.Tok, comments[i].Pos()) == safetoken.Line(pgf.Tok, end) {
+				end = comments[i].End()
+			}
+			movingRanges = append(movingRanges, movingRange{pgf: pgf, node: n, start: start, end: end, tok: tok})
 		}
 		for _, decl := range pgf.File.Decls {
 			switch decl := decl.(type) {
diff --git a/gopls/internal/test/marker/testdata/codeaction/dialog_move_decl_comments.txt b/gopls/internal/test/marker/testdata/codeaction/dialog_move_decl_comments.txt
new file mode 100644
index 0000000..78493fe
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/codeaction/dialog_move_decl_comments.txt
@@ -0,0 +1,214 @@
+Test move declaration refactoring: doc comments and trailing comments should move with their declarations.
+
+-- capabilities.json --
+{
+	"experimental":{"interactiveResolve":{"inputTypes":["string"]}}
+}
+
+-- settings.json --
+{
+	"moveDeclaration": true
+}
+
+-- flags --
+-ignore_extra_diags
+
+-- go.mod --
+module golang.org/lsptests/movedeclcomments
+
+go 1.20
+
+-- deps/dest/dest.go --
+package dest
+
+func Existing() {}
+
+-- deps/src/src.go --
+package src
+
+// Target's dependencies move with their doc and trailing comments.
+// Stays does not move, so its comments stay too.
+func Target() int { //@codeaction(re"Target", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/deps/dest/dest.go"}`, result=comments_deps)
+	return dep + blockDep + depStruct{}.n
+}
+
+// dep's doc comment.
+var dep = 1 // dep's trailing comment
+
+var blockDep = 2 /* blockDep's trailing block comment */
+
+// depStruct's doc comment.
+type depStruct struct {
+	n int // field comment
+} // depStruct's trailing comment
+
+// Stays's doc comment.
+func Stays() {} // Stays's trailing comment
+
+-- @comments_deps/deps/dest/dest.go --
+package dest
+
+func Existing() {}
+
+// Target's dependencies move with their doc and trailing comments.
+// Stays does not move, so its comments stay too.
+func Target() int { //@codeaction(re"Target", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/deps/dest/dest.go"}`, result=comments_deps)
+	return dep + blockDep + depStruct{}.n
+}
+
+// dep's doc comment.
+var dep = 1 // dep's trailing comment
+
+var blockDep = 2 /* blockDep's trailing block comment */
+
+// depStruct's doc comment.
+type depStruct struct {
+	n int // field comment
+} // depStruct's trailing comment
+-- @comments_deps/deps/src/src.go --
+package src
+
+// Stays's doc comment.
+func Stays() {} // Stays's trailing comment
+
+-- nextline/dest/dest.go --
+package dest
+
+func Existing() {}
+
+-- nextline/src/src.go --
+package src
+
+func NextLine() { //@codeaction(re"NextLine", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/nextline/dest/dest.go"}`, result=comments_next_line)
+}
+// This comment is on the line after NextLine's closing brace, so it is not
+// a trailing comment and stays in src.
+
+func Other() {}
+
+-- @comments_next_line/nextline/dest/dest.go --
+package dest
+
+func Existing() {}
+
+func NextLine() { //@codeaction(re"NextLine", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/nextline/dest/dest.go"}`, result=comments_next_line)
+}
+-- @comments_next_line/nextline/src/src.go --
+package src
+
+// This comment is on the line after NextLine's closing brace, so it is not
+// a trailing comment and stays in src.
+
+func Other() {}
+
+-- spec/dest/dest.go --
+package dest
+
+func Existing() {}
+
+-- spec/src/spec.go --
+package src
+
+func UsesSecond() int { //@codeaction(re"UsesSecond", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/spec/dest/dest.go"}`, result=comments_spec)
+	return Second
+}
+
+var (
+	// First's doc comment.
+	First = 1 // First's trailing comment
+	// Second's doc comment goes before the "var" keyword added in dest.
+	Second = 2 // Second's trailing comment
+)
+
+-- @comments_spec/spec/dest/dest.go --
+package dest
+
+func Existing() {}
+
+func UsesSecond() int { //@codeaction(re"UsesSecond", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/spec/dest/dest.go"}`, result=comments_spec)
+	return Second
+}
+
+// Second's doc comment goes before the "var" keyword added in dest.
+	var Second = 2 // Second's trailing comment
+-- @comments_spec/spec/src/spec.go --
+package src
+
+var (
+	// First's doc comment.
+	First = 1 // First's trailing comment
+	)
+
+-- groups/dest/dest.go --
+package dest
+
+func Existing() {}
+
+-- groups/src/src.go --
+package src
+
+func UsesGroups() int { //@codeaction(re"UsesGroups", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/groups/dest/dest.go"}`, result=comments_groups)
+	return wholeA + partB
+}
+
+// The whole group moves, with its doc comment.
+var (
+	wholeA = wholeB
+	wholeB = 1
+) // The whole group's trailing comment moves too.
+
+// Only partB moves, so this group doc comment stays with partA.
+var (
+	partA = 1 // partA's trailing comment
+	partB = 2 // partB's trailing comment
+) // This group's trailing comment stays.
+-- @comments_groups/groups/dest/dest.go --
+package dest
+
+func Existing() {}
+
+func UsesGroups() int { //@codeaction(re"UsesGroups", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/groups/dest/dest.go"}`, result=comments_groups)
+	return wholeA + partB
+}
+
+// The whole group moves, with its doc comment.
+var (
+	wholeA = wholeB
+	wholeB = 1
+) // The whole group's trailing comment moves too.
+
+var partB = 2 // partB's trailing comment
+-- @comments_groups/groups/src/src.go --
+package src
+
+// Only partB moves, so this group doc comment stays with partA.
+var (
+	partA = 1 // partA's trailing comment
+	) // This group's trailing comment stays.
+-- header/src.go --
+// Copyright 2026 The Go Authors. All rights reserved.
+
+//go:build go1.18
+
+package header
+
+// Moving Header to a new file in the same package copies this file's
+// copyright and build constraint to the new file.
+func Header() {} //@codeaction(re"Header", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/header/header_new.go"}`, result=comments_header)
+-- @comments_header/header/src.go --
+// Copyright 2026 The Go Authors. All rights reserved.
+
+//go:build go1.18
+
+package header
+
+-- @comments_header/header/header_new.go --
+// Copyright 2026 The Go Authors. All rights reserved.
+
+//go:build go1.18
+
+package header
+
+// Moving Header to a new file in the same package copies this file's
+// copyright and build constraint to the new file.
+func Header() {} //@codeaction(re"Header", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/header/header_new.go"}`, result=comments_header)
diff --git a/gopls/internal/test/marker/testdata/codeaction/dialog_move_decl_edits.txt b/gopls/internal/test/marker/testdata/codeaction/dialog_move_decl_edits.txt
index 71c352d..222c23a 100644
--- a/gopls/internal/test/marker/testdata/codeaction/dialog_move_decl_edits.txt
+++ b/gopls/internal/test/marker/testdata/codeaction/dialog_move_decl_edits.txt
@@ -28,8 +28,6 @@
 -- @move_func/src/func.go --
 package src
 
-
-
 -- src/type.go --
 package src
 
@@ -41,8 +39,6 @@
 -- @move_type/src/type.go --
 package src
 
-
-
 -- src/newfile.go --
 package src
 
@@ -54,9 +50,6 @@
 -- @move_new_file/src/newfile.go --
 package src
 
-// Cross-package move to a new file: dest/newfile.go does not exist yet and must be created.
-
-
 -- src/samepkg_new.go --
 package src
 
@@ -68,9 +61,6 @@
 -- @move_samepkg_new_file/src/samepkg_new.go --
 package src
 
-// Same-package move to a new file: src/created.go does not exist yet and must be created.
-
-
 -- src/header.go --
 // Copyright 2026 The Go Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style
@@ -94,14 +84,13 @@
 
 package src
 
-
-
-
 -- src/spec.go --
 package src
 
 type (
 	First int
+	// Second has a doc comment
+	// that spans multiple lines.
 	Second string //@codeaction(re"Second", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_subset_spec)
 )
 
@@ -110,8 +99,7 @@
 
 type (
 	First int
-	 //@codeaction(re"Second", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_subset_spec)
-)
+	)
 
 -- src/counter.go --
 package src
@@ -127,10 +115,6 @@
 -- @move_decl_with_method/src/counter.go --
 package src
 
-
-
-
-
 -- src/iota.go --
 package src
 
@@ -150,8 +134,6 @@
 -- @move_to_test_file/src/helper.go --
 package src
 
-
-
 -- src/helper_test.go --
 package src
 
@@ -175,7 +157,7 @@
 
 func Existing() {}
 
-var Mover    = mover
+var Mover    = mover //@codeaction(re"Mover", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_spec_subset_multi)
 
 var mover    = 2
 -- @move_spec_subset_multi/src/spec_group.go --
@@ -184,8 +166,6 @@
 // Moving Mover also moves its dependency mover: two of the four specs move.
 var (
 	Stay     = 1
-	 //@codeaction(re"Mover", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_spec_subset_multi)
-	
 	AlsoStay = 3
 )
 
@@ -203,6 +183,7 @@
 
 func Existing() {}
 
+// Moving Whole also moves whole, so every spec in the group moves.
 var (
 	Whole = whole //@codeaction(re"Whole", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_whole_gendecl)
 	whole = 1
@@ -210,9 +191,6 @@
 -- @move_whole_gendecl/src/whole_group.go --
 package src
 
-// Moving Whole also moves whole, so every spec in the group moves.
-
-
 -- src/middle.go --
 package src
 
@@ -237,8 +215,6 @@
 // Only Middle is removed; its neighbors stay.
 func Above() {}
 
-
-
 func Below() {}
 
 -- src/split_type.go --
@@ -255,21 +231,17 @@
 
 func (s Split) Method() {}
 
+// Split's method is in another file; both are removed from their own files.
 type Split struct { //@codeaction(re"Split", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_remove_split)
 }
 -- @move_remove_split/src/split_methods.go --
 package src
 
-
-
 func KeepMe() {}
 
 -- @move_remove_split/src/split_type.go --
 package src
 
-// Split's method is in another file; both are removed from their own files.
-
-
 -- src/split_methods.go --
 package src
 
@@ -312,12 +284,14 @@
 -- @move_new_file/dest/newfile.go --
 package dest
 
+// Cross-package move to a new file: dest/newfile.go does not exist yet and must be created.
 func Multiply(x, y int) int { //@codeaction(re"Multiply", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/newfile.go"}`, result=move_new_file)
 	return x * y
 }
 -- @move_samepkg_new_file/src/created.go --
 package src
 
+// Same-package move to a new file: src/created.go does not exist yet and must be created.
 func Separate() string { //@codeaction(re"Separate", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/src/created.go"}`, result=move_samepkg_new_file)
 	return "same package new file"
 }
@@ -338,7 +312,9 @@
 
 func Existing() {}
 
-type Second string
+// Second has a doc comment
+	// that spans multiple lines.
+	type Second string //@codeaction(re"Second", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_subset_spec)
 -- @move_decl_with_method/dest/dest.go --
 package dest
 
diff --git a/gopls/internal/test/marker/testdata/codeaction/dialog_move_decl_imports.txt b/gopls/internal/test/marker/testdata/codeaction/dialog_move_decl_imports.txt
index 61eb3b8..d527246 100644
--- a/gopls/internal/test/marker/testdata/codeaction/dialog_move_decl_imports.txt
+++ b/gopls/internal/test/marker/testdata/codeaction/dialog_move_decl_imports.txt
@@ -77,8 +77,6 @@
 
 import "fmt"
 
-
-
 func StayMe() {
 	fmt.Println("staying")
 }
@@ -116,6 +114,7 @@
 
 func Existing() {}
 
+// Widget's method is in another file, and both files import "bytes".
 type Widget struct { //@codeaction(re"Widget", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_multi_file_import)
 	buf *bytes.Buffer
 }
@@ -128,9 +127,6 @@
 
 
 
-// Widget's method is in another file, and both files import "bytes".
-
-
 -- @move_multi_file_import/src/widget_methods.go --
 package src
 
@@ -139,8 +135,6 @@
 	"strings"
 )
 
-
-
 func StayUpper(s string) string {
 	return strings.ToUpper(s)
 }
@@ -180,6 +174,7 @@
 
 func Existing() {}
 
+// UsesBytes's method is in another file, which imports "bytes" as bytes2.
 type UsesBytes struct { //@codeaction(re"UsesBytes", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_multi_file_renamed_import)
 	buf *bytes.Buffer
 }
@@ -192,16 +187,11 @@
 
 
 
-// UsesBytes's method is in another file, which imports "bytes" as bytes2.
-
-
 -- @move_multi_file_renamed_import/src/gadget_methods.go --
 package src
 
 
 
-
-
 -- src/gadget_methods.go --
 package src
 
@@ -233,6 +223,8 @@
 
 func Existing() {}
 
+// UsesDest refers to dest in its signature and body. The dest. qualifiers
+// are dropped, and the dest import is removed from src.
 func UsesDest(t Thing) Thing { //@codeaction(re"UsesDest", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_uses_dest)
 	Existing()
 	return t
@@ -242,10 +234,6 @@
 
 
 
-// UsesDest refers to dest in its signature and body. The dest. qualifiers
-// are dropped, and the dest import is removed from src.
-
-
 -- src/uses_dest_renamed.go --
 package src
 
@@ -261,6 +249,7 @@
 
 func Existing() {}
 
+// UsesDestRenamed refers to dest through a renamed import.
 func UsesDestRenamed() { //@codeaction(re"UsesDestRenamed", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_uses_dest_renamed)
 	Existing()
 }
@@ -269,9 +258,6 @@
 
 
 
-// UsesDestRenamed refers to dest through a renamed import.
-
-
 -- src/uses_dest_shared.go --
 package src
 
@@ -292,6 +278,8 @@
 
 func Existing() {}
 
+// MovesWithDest drops its dest. qualifier, but src keeps the dest import
+// because StaysWithDest still uses it.
 func MovesWithDest() { //@codeaction(re"MovesWithDest", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_uses_dest_shared)
 	Existing()
 }
@@ -300,10 +288,6 @@
 
 import "golang.org/lsptests/movedeclimports/dest"
 
-// MovesWithDest drops its dest. qualifier, but src keeps the dest import
-// because StaysWithDest still uses it.
-
-
 func StaysWithDest() {
 	dest.Existing()
 }
@@ -334,6 +318,7 @@
 
 func Existing() {}
 
+// CallsDest refers to the dest package, which must not import itself.
 func CallsDest() { //@codeaction(re"CallsDest", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_no_self_import)
 	Existing()
 }
@@ -342,9 +327,6 @@
 
 
 
-// CallsDest refers to the dest package, which must not import itself.
-
-
 -- src/commented.go --
 package src
 
@@ -362,6 +344,7 @@
 
 func Existing() {}
 
+// Commented's method is in another file, which imports "bytes" without a comment.
 type Commented struct { //@codeaction(re"Commented", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_commented_import)
 	buf *bytes.Buffer
 }
@@ -374,16 +357,11 @@
 
  // for Buffer
 
-// Commented's method is in another file, which imports "bytes" without a comment.
-
-
 -- @move_commented_import/src/commented_methods.go --
 package src
 
 
 
-
-
 -- src/commented_methods.go --
 package src
 
@@ -428,6 +406,7 @@
 
 func Existing() {}
 
+// Describe needs several imports. Their order in dest must be deterministic.
 func Describe(b *bytes.Buffer) error { //@codeaction(re"Describe", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/describe.go"}`, result=move_import_order_new), codeaction(re"Describe", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_import_order_existing)
 	if b == nil {
 		return errors.New("nil buffer")
@@ -446,9 +425,6 @@
 	
 )
 
-// Describe needs several imports. Their order in dest must be deterministic.
-
-
 -- @move_import_order_new/dest/describe.go --
 package dest
 
@@ -460,6 +436,7 @@
 	"unicode"
 )
 
+// Describe needs several imports. Their order in dest must be deterministic.
 func Describe(b *bytes.Buffer) error { //@codeaction(re"Describe", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/describe.go"}`, result=move_import_order_new), codeaction(re"Describe", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_import_order_existing)
 	if b == nil {
 		return errors.New("nil buffer")
@@ -478,16 +455,11 @@
 	
 )
 
-// Describe needs several imports. Their order in dest must be deterministic.
-
-
 -- @move_single_import/src/single.go --
 package src
 
 
 
-
-
 -- @move_single_import/dest/dest.go --
 package dest
 
@@ -503,8 +475,6 @@
 
 
 
-
-
 -- @move_dup_import/dest/has_import.go --
 package dest
 
@@ -522,8 +492,6 @@
 
 
 
-
-
 -- @move_new_file_import/dest/newfile.go --
 package dest
 
@@ -552,8 +520,6 @@
 	"strings"
 )
 
-
-
 func StayStrings(s string) bool {
 	return strings.Contains(s, "x")
 }
@@ -573,7 +539,7 @@
 
 func Existing() {}
 
-var DestThing Thing
+var DestThing Thing //@codeaction(re"DestThing", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_uses_dest_spec)
 -- @move_uses_dest_spec/src/uses_dest_spec.go --
 package src
 
@@ -581,7 +547,6 @@
 
 // Only DestThing moves out of the group; its dest. qualifier is dropped.
 var (
-	 //@codeaction(re"DestThing", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=move_uses_dest_spec)
 	OtherThing dest.Thing
 )
 
diff --git a/gopls/internal/test/marker/testdata/codeaction/dialog_move_decl_refs.txt b/gopls/internal/test/marker/testdata/codeaction/dialog_move_decl_refs.txt
index 7b1effc..7b11765 100644
--- a/gopls/internal/test/marker/testdata/codeaction/dialog_move_decl_refs.txt
+++ b/gopls/internal/test/marker/testdata/codeaction/dialog_move_decl_refs.txt
@@ -52,8 +52,6 @@
 
 import "golang.org/lsptests/movedeclrefs/dest2"
 
-
-
 func CallerSameFile() int {
 	return dest2.TargetInSrc()
 }
@@ -148,8 +146,6 @@
 -- @ref_in_dest/src/to_dest.go --
 package src
 
-
-
 -- dest/use_src.go --
 package dest
 
@@ -200,8 +196,6 @@
 	return 1
 }
 
-
-
 func MoveForExtShared() int { //@codeaction(re"MoveForExtShared", "refactor.move.moveDeclaration", answers=`{"file":"file://$WORKDIR/dest/dest.go"}`, result=ref_in_ext_keep_src)
 	return 3
 }
@@ -236,8 +230,6 @@
 	return 2
 }
 
-
-
 -- ext/only_moving.go --
 package ext
 
@@ -288,10 +280,6 @@
 
 import "golang.org/lsptests/movedeclrefs/dest2"
 
-
-
-
-
 func UseCounterInSrc() {
 	var c dest2.Counter
 	c.Inc()