gopls/internal: add code action "extract declarations to new file"

This code action moves selected code sections to a newly created file within the same package. The created filename is chosen as the first {function, type, const, var} name encountered. In addition, import declarations are added or removed as needed.

Fixes golang/go#65707

Change-Id: I3fd45afd3569e4e0cee17798a48bde6916eb57b8
GitHub-Last-Rev: e551a8a24f3dc5ea51e1c54f9f28c147c7669b11
GitHub-Pull-Request: golang/tools#479
Reviewed-on: https://go-review.googlesource.com/c/tools/+/565895
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>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md
index 7488e1f..90f70d3 100644
--- a/gopls/doc/commands.md
+++ b/gopls/doc/commands.md
@@ -291,6 +291,28 @@
 }
 ```
 
+## `gopls.extract_to_new_file`: **Move selected declarations to a new file**
+
+Used by the code action of the same name.
+
+Args:
+
+```
+{
+	"uri": string,
+	"range": {
+		"start": {
+			"line": uint32,
+			"character": uint32,
+		},
+		"end": {
+			"line": uint32,
+			"character": uint32,
+		},
+	},
+}
+```
+
 ## `gopls.fetch_vulncheck_result`: **Get known vulncheck result**
 
 Fetch the result of latest vulnerability check (`govulncheck`).
diff --git a/gopls/doc/release/v0.17.0.md b/gopls/doc/release/v0.17.0.md
index 3fbfa73..fff5798 100644
--- a/gopls/doc/release/v0.17.0.md
+++ b/gopls/doc/release/v0.17.0.md
@@ -5,3 +5,20 @@
 The `fieldalignment` analyzer, previously disabled by default, has
 been removed: it is redundant with the hover size/offset information
 displayed by v0.16.0 and its diagnostics were confusing.
+
+
+# New features
+
+## Extract declarations to new file
+Gopls now offers another code action, "Extract declarations to new file",
+which moves selected code sections to a newly created file within the
+same package. The created filename is chosen as the first {function, type, 
+const, var} name encountered. In addition, import declarations are added or
+removed as needed.
+
+The user can invoke this code action by selecting a function name, the keywords
+`func`, `const`, `var`, `type`, or by placing the caret on them without selecting,
+or by selecting a whole declaration or multiple declrations.
+
+In order to avoid ambiguity and surprise about what to extract, some kinds
+of paritial selection of a declration cannot invoke this code action.
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json
index 2745a43..ffe9e33 100644
--- a/gopls/internal/doc/api.json
+++ b/gopls/internal/doc/api.json
@@ -1011,6 +1011,13 @@
 			"ResultDoc": ""
 		},
 		{
+			"Command": "gopls.extract_to_new_file",
+			"Title": "Move selected declarations to a new file",
+			"Doc": "Used by the code action of the same name.",
+			"ArgDoc": "{\n\t\"uri\": string,\n\t\"range\": {\n\t\t\"start\": {\n\t\t\t\"line\": uint32,\n\t\t\t\"character\": uint32,\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": uint32,\n\t\t\t\"character\": uint32,\n\t\t},\n\t},\n}",
+			"ResultDoc": ""
+		},
+		{
 			"Command": "gopls.fetch_vulncheck_result",
 			"Title": "Get known vulncheck result",
 			"Doc": "Fetch the result of latest vulnerability check (`govulncheck`).",
diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go
index c4990ae..bf26458 100644
--- a/gopls/internal/golang/codeaction.go
+++ b/gopls/internal/golang/codeaction.go
@@ -240,10 +240,6 @@
 
 // getExtractCodeActions returns any refactor.extract code actions for the selection.
 func getExtractCodeActions(pgf *parsego.File, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) {
-	if rng.Start == rng.End {
-		return nil, nil
-	}
-
 	start, end, err := pgf.RangePos(rng)
 	if err != nil {
 		return nil, err
@@ -286,6 +282,16 @@
 		}
 		commands = append(commands, cmd)
 	}
+	if canExtractToNewFile(pgf, start, end) {
+		cmd, err := command.NewExtractToNewFileCommand(
+			"Extract declarations to new file",
+			protocol.Location{URI: pgf.URI, Range: rng},
+		)
+		if err != nil {
+			return nil, err
+		}
+		commands = append(commands, cmd)
+	}
 	var actions []protocol.CodeAction
 	for i := range commands {
 		actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorExtract, &commands[i], nil, options))
diff --git a/gopls/internal/golang/extracttofile.go b/gopls/internal/golang/extracttofile.go
new file mode 100644
index 0000000..9b3aad5
--- /dev/null
+++ b/gopls/internal/golang/extracttofile.go
@@ -0,0 +1,302 @@
+// Copyright 2024 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
+
+// This file defines the code action "Extract declarations to new file".
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"go/ast"
+	"go/format"
+	"go/token"
+	"go/types"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"golang.org/x/tools/gopls/internal/cache"
+	"golang.org/x/tools/gopls/internal/cache/parsego"
+	"golang.org/x/tools/gopls/internal/file"
+	"golang.org/x/tools/gopls/internal/protocol"
+	"golang.org/x/tools/gopls/internal/util/bug"
+	"golang.org/x/tools/gopls/internal/util/safetoken"
+	"golang.org/x/tools/gopls/internal/util/typesutil"
+)
+
+// canExtractToNewFile reports whether the code in the given range can be extracted to a new file.
+func canExtractToNewFile(pgf *parsego.File, start, end token.Pos) bool {
+	_, _, _, ok := selectedToplevelDecls(pgf, start, end)
+	return ok
+}
+
+// findImportEdits finds imports specs that needs to be added to the new file
+// or deleted from the old file if the range is extracted to a new file.
+//
+// TODO: handle dot imports.
+func findImportEdits(file *ast.File, info *types.Info, start, end token.Pos) (adds, deletes []*ast.ImportSpec, _ error) {
+	// make a map from a pkgName to its references
+	pkgNameReferences := make(map[*types.PkgName][]*ast.Ident)
+	for ident, use := range info.Uses {
+		if pkgName, ok := use.(*types.PkgName); ok {
+			pkgNameReferences[pkgName] = append(pkgNameReferences[pkgName], ident)
+		}
+	}
+
+	// PkgName referenced in the extracted selection must be
+	// imported in the new file.
+	// PkgName only referenced in the extracted selection must be
+	// deleted from the original file.
+	for _, spec := range file.Imports {
+		if spec.Name != nil && spec.Name.Name == "." {
+			// TODO: support dot imports.
+			return nil, nil, errors.New("\"extract to new file\" does not support files containing dot imports")
+		}
+		pkgName, ok := typesutil.ImportedPkgName(info, spec)
+		if !ok {
+			continue
+		}
+		usedInSelection := false
+		usedInNonSelection := false
+		for _, ident := range pkgNameReferences[pkgName] {
+			if posRangeContains(start, end, ident.Pos(), ident.End()) {
+				usedInSelection = true
+			} else {
+				usedInNonSelection = true
+			}
+		}
+		if usedInSelection {
+			adds = append(adds, spec)
+		}
+		if usedInSelection && !usedInNonSelection {
+			deletes = append(deletes, spec)
+		}
+	}
+
+	return adds, deletes, nil
+}
+
+// ExtractToNewFile moves selected declarations into a new file.
+func ExtractToNewFile(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range) (*protocol.WorkspaceEdit, error) {
+	errorPrefix := "ExtractToNewFile"
+
+	pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
+	if err != nil {
+		return nil, err
+	}
+
+	start, end, err := pgf.RangePos(rng)
+	if err != nil {
+		return nil, fmt.Errorf("%s: %w", errorPrefix, err)
+	}
+
+	start, end, firstSymbol, ok := selectedToplevelDecls(pgf, start, end)
+	if !ok {
+		return nil, bug.Errorf("invalid selection")
+	}
+
+	// select trailing empty lines
+	offset, err := safetoken.Offset(pgf.Tok, end)
+	if err != nil {
+		return nil, err
+	}
+	rest := pgf.Src[offset:]
+	end += token.Pos(len(rest) - len(bytes.TrimLeft(rest, " \t\n")))
+
+	replaceRange, err := pgf.PosRange(start, end)
+	if err != nil {
+		return nil, bug.Errorf("invalid range: %v", err)
+	}
+
+	adds, deletes, err := findImportEdits(pgf.File, pkg.TypesInfo(), start, end)
+	if err != nil {
+		return nil, err
+	}
+
+	var importDeletes []protocol.TextEdit
+	// For unparenthesised declarations like `import "fmt"` we remove
+	// the whole declaration because simply removing importSpec leaves
+	// `import \n`, which does not compile.
+	// For parenthesised declarations like `import ("fmt"\n "log")`
+	// we only remove the ImportSpec, because removing the whole declaration
+	// might remove other ImportsSpecs we don't want to touch.
+	unparenthesizedImports := unparenthesizedImports(pgf)
+	for _, importSpec := range deletes {
+		if decl := unparenthesizedImports[importSpec]; decl != nil {
+			importDeletes = append(importDeletes, removeNode(pgf, decl))
+		} else {
+			importDeletes = append(importDeletes, removeNode(pgf, importSpec))
+		}
+	}
+
+	var buf bytes.Buffer
+	fmt.Fprintf(&buf, "package %s\n", pgf.File.Name.Name)
+	if len(adds) > 0 {
+		buf.WriteString("import (")
+		for _, importSpec := range adds {
+			if importSpec.Name != nil {
+				fmt.Fprintf(&buf, "%s %s\n", importSpec.Name.Name, importSpec.Path.Value)
+			} else {
+				fmt.Fprintf(&buf, "%s\n", importSpec.Path.Value)
+			}
+		}
+		buf.WriteString(")\n")
+	}
+
+	newFile, err := chooseNewFile(ctx, snapshot, pgf.URI.Dir().Path(), firstSymbol)
+	if err != nil {
+		return nil, fmt.Errorf("%s: %w", errorPrefix, err)
+	}
+
+	fileStart := pgf.Tok.Pos(0) // TODO(adonovan): use go1.20 pgf.File.FileStart
+	buf.Write(pgf.Src[start-fileStart : end-fileStart])
+
+	// TODO: attempt to duplicate the copyright header, if any.
+	newFileContent, err := format.Source(buf.Bytes())
+	if err != nil {
+		return nil, err
+	}
+
+	return protocol.NewWorkspaceEdit(
+		// edit the original file
+		protocol.DocumentChangeEdit(fh, append(importDeletes, protocol.TextEdit{Range: replaceRange, NewText: ""})),
+		// create a new file
+		protocol.DocumentChangeCreate(newFile.URI()),
+		// edit the created file
+		protocol.DocumentChangeEdit(newFile, []protocol.TextEdit{
+			{Range: protocol.Range{}, NewText: string(newFileContent)},
+		})), nil
+}
+
+// chooseNewFile chooses a new filename in dir, based on the name of the
+// first extracted symbol, and if necessary to disambiguate, a numeric suffix.
+func chooseNewFile(ctx context.Context, snapshot *cache.Snapshot, dir string, firstSymbol string) (file.Handle, error) {
+	basename := strings.ToLower(firstSymbol)
+	newPath := protocol.URIFromPath(filepath.Join(dir, basename+".go"))
+	for count := 1; count < 5; count++ {
+		fh, err := snapshot.ReadFile(ctx, newPath)
+		if err != nil {
+			return nil, err // canceled
+		}
+		if _, err := fh.Content(); errors.Is(err, os.ErrNotExist) {
+			return fh, nil
+		}
+		filename := fmt.Sprintf("%s.%d.go", basename, count)
+		newPath = protocol.URIFromPath(filepath.Join(dir, filename))
+	}
+	return nil, fmt.Errorf("chooseNewFileURI: exceeded retry limit")
+}
+
+// selectedToplevelDecls returns the lexical extent of the top-level
+// declarations enclosed by [start, end), along with the name of the
+// first declaration. The returned boolean reports whether the selection
+// should be offered a code action to extract the declarations.
+func selectedToplevelDecls(pgf *parsego.File, start, end token.Pos) (token.Pos, token.Pos, string, bool) {
+	// selection cannot intersect a package declaration
+	if posRangeIntersects(start, end, pgf.File.Package, pgf.File.Name.End()) {
+		return 0, 0, "", false
+	}
+	firstName := ""
+	for _, decl := range pgf.File.Decls {
+		if posRangeIntersects(start, end, decl.Pos(), decl.End()) {
+			var id *ast.Ident
+			switch v := decl.(type) {
+			case *ast.BadDecl:
+				return 0, 0, "", false
+			case *ast.FuncDecl:
+				// if only selecting keyword "func" or function name, extend selection to the
+				// whole function
+				if posRangeContains(v.Pos(), v.Name.End(), start, end) {
+					start, end = v.Pos(), v.End()
+				}
+				id = v.Name
+			case *ast.GenDecl:
+				// selection cannot intersect an import declaration
+				if v.Tok == token.IMPORT {
+					return 0, 0, "", false
+				}
+				// if only selecting keyword "type", "const", or "var", extend selection to the
+				// whole declaration
+				if v.Tok == token.TYPE && posRangeContains(v.Pos(), v.Pos()+token.Pos(len("type")), start, end) ||
+					v.Tok == token.CONST && posRangeContains(v.Pos(), v.Pos()+token.Pos(len("const")), start, end) ||
+					v.Tok == token.VAR && posRangeContains(v.Pos(), v.Pos()+token.Pos(len("var")), start, end) {
+					start, end = v.Pos(), v.End()
+				}
+				if len(v.Specs) > 0 {
+					switch spec := v.Specs[0].(type) {
+					case *ast.TypeSpec:
+						id = spec.Name
+					case *ast.ValueSpec:
+						id = spec.Names[0]
+					}
+				}
+			}
+			// selection cannot partially intersect a node
+			if !posRangeContains(start, end, decl.Pos(), decl.End()) {
+				return 0, 0, "", false
+			}
+			if id != nil && firstName == "" {
+				// may be "_"
+				firstName = id.Name
+			}
+			// extends selection to docs comments
+			var c *ast.CommentGroup
+			switch decl := decl.(type) {
+			case *ast.GenDecl:
+				c = decl.Doc
+			case *ast.FuncDecl:
+				c = decl.Doc
+			}
+			if c != nil && c.Pos() < start {
+				start = c.Pos()
+			}
+		}
+	}
+	for _, comment := range pgf.File.Comments {
+		if posRangeIntersects(start, end, comment.Pos(), comment.End()) {
+			if !posRangeContains(start, end, comment.Pos(), comment.End()) {
+				// selection cannot partially intersect a comment
+				return 0, 0, "", false
+			}
+		}
+	}
+	if firstName == "" {
+		return 0, 0, "", false
+	}
+	return start, end, firstName, true
+}
+
+// unparenthesizedImports returns a map from each unparenthesized ImportSpec
+// to its enclosing declaration (which may need to be deleted too).
+func unparenthesizedImports(pgf *parsego.File) map[*ast.ImportSpec]*ast.GenDecl {
+	decls := make(map[*ast.ImportSpec]*ast.GenDecl)
+	for _, decl := range pgf.File.Decls {
+		if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.IMPORT && !decl.Lparen.IsValid() {
+			decls[decl.Specs[0].(*ast.ImportSpec)] = decl
+		}
+	}
+	return decls
+}
+
+// removeNode returns a TextEdit that removes the node.
+func removeNode(pgf *parsego.File, node ast.Node) protocol.TextEdit {
+	rng, err := pgf.NodeRange(node)
+	if err != nil {
+		bug.Reportf("removeNode: %v", err)
+	}
+	return protocol.TextEdit{Range: rng, NewText: ""}
+}
+
+// posRangeIntersects checks if [a, b) and [c, d) intersects, assuming a <= b and c <= d.
+func posRangeIntersects(a, b, c, d token.Pos) bool {
+	return !(b <= c || d <= a)
+}
+
+// posRangeContains checks if [a, b) contains [c, d), assuming a <= b and c <= d.
+func posRangeContains(a, b, c, d token.Pos) bool {
+	return a <= c && d <= b
+}
diff --git a/gopls/internal/protocol/command/command_gen.go b/gopls/internal/protocol/command/command_gen.go
index ee937fc..c9ecf7b 100644
--- a/gopls/internal/protocol/command/command_gen.go
+++ b/gopls/internal/protocol/command/command_gen.go
@@ -34,6 +34,7 @@
 	DiagnoseFiles           Command = "gopls.diagnose_files"
 	Doc                     Command = "gopls.doc"
 	EditGoDirective         Command = "gopls.edit_go_directive"
+	ExtractToNewFile        Command = "gopls.extract_to_new_file"
 	FetchVulncheckResult    Command = "gopls.fetch_vulncheck_result"
 	FreeSymbols             Command = "gopls.free_symbols"
 	GCDetails               Command = "gopls.gc_details"
@@ -74,6 +75,7 @@
 	DiagnoseFiles,
 	Doc,
 	EditGoDirective,
+	ExtractToNewFile,
 	FetchVulncheckResult,
 	FreeSymbols,
 	GCDetails,
@@ -167,6 +169,12 @@
 			return nil, err
 		}
 		return nil, s.EditGoDirective(ctx, a0)
+	case ExtractToNewFile:
+		var a0 protocol.Location
+		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
+			return nil, err
+		}
+		return nil, s.ExtractToNewFile(ctx, a0)
 	case FetchVulncheckResult:
 		var a0 URIArg
 		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
@@ -436,6 +444,18 @@
 	}, nil
 }
 
+func NewExtractToNewFileCommand(title string, a0 protocol.Location) (protocol.Command, error) {
+	args, err := MarshalArgs(a0)
+	if err != nil {
+		return protocol.Command{}, err
+	}
+	return protocol.Command{
+		Title:     title,
+		Command:   ExtractToNewFile.String(),
+		Arguments: args,
+	}, nil
+}
+
 func NewFetchVulncheckResultCommand(title string, a0 URIArg) (protocol.Command, error) {
 	args, err := MarshalArgs(a0)
 	if err != nil {
diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go
index 54694c0..2586082 100644
--- a/gopls/internal/protocol/command/interface.go
+++ b/gopls/internal/protocol/command/interface.go
@@ -160,6 +160,11 @@
 	// themselves.
 	AddImport(context.Context, AddImportArgs) error
 
+	// ExtractToNewFile: Move selected declarations to a new file
+	//
+	// Used by the code action of the same name.
+	ExtractToNewFile(context.Context, protocol.Location) error
+
 	// StartDebugging: Start the gopls debug server
 	//
 	// Start the gopls debug server if it isn't running, and return the debug
diff --git a/gopls/internal/protocol/edits.go b/gopls/internal/protocol/edits.go
index 61bde3a..5f70c4e 100644
--- a/gopls/internal/protocol/edits.go
+++ b/gopls/internal/protocol/edits.go
@@ -129,6 +129,16 @@
 	}
 }
 
+// DocumentChangeCreate constructs a DocumentChange that creates a file.
+func DocumentChangeCreate(uri DocumentURI) DocumentChange {
+	return DocumentChange{
+		CreateFile: &CreateFile{
+			Kind: "create",
+			URI:  uri,
+		},
+	}
+}
+
 // DocumentChangeRename constructs a DocumentChange that renames a file.
 func DocumentChangeRename(src, dst DocumentURI) DocumentChange {
 	return DocumentChange{
diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go
index 46a47b7..d53b975 100644
--- a/gopls/internal/server/command.go
+++ b/gopls/internal/server/command.go
@@ -940,6 +940,26 @@
 	})
 }
 
+func (c *commandHandler) ExtractToNewFile(ctx context.Context, args protocol.Location) error {
+	return c.run(ctx, commandConfig{
+		progress: "Extract to a new file",
+		forURI:   args.URI,
+	}, func(ctx context.Context, deps commandDeps) error {
+		edit, err := golang.ExtractToNewFile(ctx, deps.snapshot, deps.fh, args.Range)
+		if err != nil {
+			return err
+		}
+		resp, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{Edit: *edit})
+		if err != nil {
+			return fmt.Errorf("could not apply edits: %v", err)
+		}
+		if !resp.Applied {
+			return fmt.Errorf("edits not applied: %s", resp.FailureReason)
+		}
+		return nil
+	})
+}
+
 func (c *commandHandler) StartDebugging(ctx context.Context, args command.DebuggingArgs) (result command.DebuggingResult, _ error) {
 	addr := args.Addr
 	if addr == "" {
diff --git a/gopls/internal/test/integration/wrappers.go b/gopls/internal/test/integration/wrappers.go
index 88145e7..3247fac 100644
--- a/gopls/internal/test/integration/wrappers.go
+++ b/gopls/internal/test/integration/wrappers.go
@@ -6,6 +6,8 @@
 
 import (
 	"encoding/json"
+	"errors"
+	"os"
 	"path"
 
 	"golang.org/x/tools/gopls/internal/protocol"
@@ -114,16 +116,24 @@
 	}
 }
 
-// ReadFile returns the file content for name that applies to the current
-// editing session: if the file is open, it returns its buffer content,
-// otherwise it returns on disk content.
+// FileContent returns the file content for name that applies to the current
+// editing session: it returns the buffer content for an open file, the
+// on-disk content for an unopened file, or "" for a non-existent file.
 func (e *Env) FileContent(name string) string {
 	e.T.Helper()
 	text, ok := e.Editor.BufferText(name)
 	if ok {
 		return text
 	}
-	return e.ReadWorkspaceFile(name)
+	content, err := e.Sandbox.Workdir.ReadFile(name)
+	if err != nil {
+		if errors.Is(err, os.ErrNotExist) {
+			return ""
+		} else {
+			e.T.Fatal(err)
+		}
+	}
+	return string(content)
 }
 
 // RegexpSearch returns the starting position of the first match for re in the
diff --git a/gopls/internal/test/marker/testdata/codeaction/extracttofile.txt b/gopls/internal/test/marker/testdata/codeaction/extracttofile.txt
new file mode 100644
index 0000000..0226d82
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/codeaction/extracttofile.txt
@@ -0,0 +1,279 @@
+This test checks the behavior of the 'extract to a new file' code action.
+
+-- flags --
+-ignore_extra_diags
+
+-- go.mod --
+module golang.org/lsptests/extracttofile
+
+go 1.18
+
+-- a.go --
+package main
+
+// docs
+func fn() {} //@codeactionedit("func", "refactor.extract", function_declaration)
+
+func fn2() {} //@codeactionedit("fn2", "refactor.extract", only_select_func_name)
+
+func fn3() {} //@codeactionedit(re`()fn3`, "refactor.extract", zero_width_selection_on_func_name)
+
+// docs
+type T int //@codeactionedit("type", "refactor.extract", type_declaration)
+
+// docs
+var V int //@codeactionedit("var", "refactor.extract", var_declaration)
+
+// docs
+const K = "" //@codeactionedit("const", "refactor.extract", const_declaration)
+
+const ( //@codeactionedit("const", "refactor.extract", const_declaration_multiple_specs)
+    P = iota
+    Q
+    R
+)
+
+func fnA () {} //@codeaction("func", mdEnd,  "refactor.extract", multiple_declarations)
+
+// unattached comment
+
+func fnB () {} //@loc(mdEnd, "}")
+
+
+-- existing.go --
+-- existing2.go --
+-- existing2.1.go --
+-- b.go --
+package main
+func existing() {} //@codeactionedit("func", "refactor.extract", file_name_conflict)
+func existing2() {} //@codeactionedit("func", "refactor.extract", file_name_conflict_again)
+
+-- single_import.go --
+package main
+import "fmt"
+func F() { //@codeactionedit("func", "refactor.extract", single_import)
+    fmt.Println()
+}
+
+-- multiple_imports.go --
+package main
+import (
+    "fmt"
+    "log"
+    time1 "time"
+)
+func init(){
+    log.Println()
+}
+func F() { //@codeactionedit("func", "refactor.extract", multiple_imports)
+    fmt.Println()
+}
+func g() string{ //@codeactionedit("func", "refactor.extract", renamed_import)
+    return time1.Now().string()
+}
+
+-- blank_import.go --
+package main
+import _ "fmt"
+func F() {} //@codeactionedit("func", "refactor.extract", blank_import)
+
+
+
+-- @blank_import/blank_import.go --
+@@ -3 +3 @@
+-func F() {} //@codeactionedit("func", "refactor.extract", blank_import)
++//@codeactionedit("func", "refactor.extract", blank_import)
+-- @blank_import/f.go --
+@@ -0,0 +1,3 @@
++package main
++
++func F() {}
+-- @const_declaration/a.go --
+@@ -16,2 +16 @@
+-// docs
+-const K = "" //@codeactionedit("const", "refactor.extract", const_declaration)
++//@codeactionedit("const", "refactor.extract", const_declaration)
+-- @const_declaration/k.go --
+@@ -0,0 +1,4 @@
++package main
++
++// docs
++const K = ""
+-- @const_declaration_multiple_specs/a.go --
+@@ -19,6 +19 @@
+-const ( //@codeactionedit("const", "refactor.extract", const_declaration_multiple_specs)
+-    P = iota
+-    Q
+-    R
+-)
+-
+-- @const_declaration_multiple_specs/p.go --
+@@ -0,0 +1,7 @@
++package main
++
++const ( //@codeactionedit("const", "refactor.extract", const_declaration_multiple_specs)
++	P = iota
++	Q
++	R
++)
+-- @file_name_conflict/b.go --
+@@ -2 +2 @@
+-func existing() {} //@codeactionedit("func", "refactor.extract", file_name_conflict)
++//@codeactionedit("func", "refactor.extract", file_name_conflict)
+-- @file_name_conflict/existing.1.go --
+@@ -0,0 +1,3 @@
++package main
++
++func existing() {}
+-- @file_name_conflict_again/b.go --
+@@ -3 +3 @@
+-func existing2() {} //@codeactionedit("func", "refactor.extract", file_name_conflict_again)
++//@codeactionedit("func", "refactor.extract", file_name_conflict_again)
+-- @file_name_conflict_again/existing2.2.go --
+@@ -0,0 +1,3 @@
++package main
++
++func existing2() {}
+-- @function_declaration/a.go --
+@@ -3,2 +3 @@
+-// docs
+-func fn() {} //@codeactionedit("func", "refactor.extract", function_declaration)
++//@codeactionedit("func", "refactor.extract", function_declaration)
+-- @function_declaration/fn.go --
+@@ -0,0 +1,4 @@
++package main
++
++// docs
++func fn() {}
+-- @multiple_declarations/a.go --
+package main
+
+// docs
+func fn() {} //@codeactionedit("func", "refactor.extract", function_declaration)
+
+func fn2() {} //@codeactionedit("fn2", "refactor.extract", only_select_func_name)
+
+func fn3() {} //@codeactionedit(re`()fn3`, "refactor.extract", zero_width_selection_on_func_name)
+
+// docs
+type T int //@codeactionedit("type", "refactor.extract", type_declaration)
+
+// docs
+var V int //@codeactionedit("var", "refactor.extract", var_declaration)
+
+// docs
+const K = "" //@codeactionedit("const", "refactor.extract", const_declaration)
+
+const ( //@codeactionedit("const", "refactor.extract", const_declaration_multiple_specs)
+    P = iota
+    Q
+    R
+)
+
+//@loc(mdEnd, "}")
+
+
+-- @multiple_declarations/fna.go --
+package main
+
+func fnA() {} //@codeaction("func", mdEnd,  "refactor.extract", multiple_declarations)
+
+// unattached comment
+
+func fnB() {}
+-- @multiple_imports/f.go --
+@@ -0,0 +1,9 @@
++package main
++
++import (
++	"fmt"
++)
++
++func F() { //@codeactionedit("func", "refactor.extract", multiple_imports)
++	fmt.Println()
++}
+-- @multiple_imports/multiple_imports.go --
+@@ -3 +3 @@
+-    "fmt"
++    
+@@ -10,3 +10 @@
+-func F() { //@codeactionedit("func", "refactor.extract", multiple_imports)
+-    fmt.Println()
+-}
+-- @only_select_func_name/a.go --
+@@ -6 +6 @@
+-func fn2() {} //@codeactionedit("fn2", "refactor.extract", only_select_func_name)
++//@codeactionedit("fn2", "refactor.extract", only_select_func_name)
+-- @only_select_func_name/fn2.go --
+@@ -0,0 +1,3 @@
++package main
++
++func fn2() {}
+-- @single_import/f.go --
+@@ -0,0 +1,9 @@
++package main
++
++import (
++	"fmt"
++)
++
++func F() { //@codeactionedit("func", "refactor.extract", single_import)
++	fmt.Println()
++}
+-- @single_import/single_import.go --
+@@ -2,4 +2 @@
+-import "fmt"
+-func F() { //@codeactionedit("func", "refactor.extract", single_import)
+-    fmt.Println()
+-}
+-- @type_declaration/a.go --
+@@ -10,2 +10 @@
+-// docs
+-type T int //@codeactionedit("type", "refactor.extract", type_declaration)
++//@codeactionedit("type", "refactor.extract", type_declaration)
+-- @type_declaration/t.go --
+@@ -0,0 +1,4 @@
++package main
++
++// docs
++type T int
+-- @var_declaration/a.go --
+@@ -13,2 +13 @@
+-// docs
+-var V int //@codeactionedit("var", "refactor.extract", var_declaration)
++//@codeactionedit("var", "refactor.extract", var_declaration)
+-- @var_declaration/v.go --
+@@ -0,0 +1,4 @@
++package main
++
++// docs
++var V int
+-- @zero_width_selection_on_func_name/a.go --
+@@ -8 +8 @@
+-func fn3() {} //@codeactionedit(re`()fn3`, "refactor.extract", zero_width_selection_on_func_name)
++//@codeactionedit(re`()fn3`, "refactor.extract", zero_width_selection_on_func_name)
+-- @zero_width_selection_on_func_name/fn3.go --
+@@ -0,0 +1,3 @@
++package main
++
++func fn3() {}
+-- @renamed_import/g.go --
+@@ -0,0 +1,9 @@
++package main
++
++import (
++	time1 "time"
++)
++
++func g() string { //@codeactionedit("func", "refactor.extract", renamed_import)
++	return time1.Now().string()
++}
+-- @renamed_import/multiple_imports.go --
+@@ -5 +5 @@
+-    time1 "time"
++    
+@@ -13,4 +13 @@
+-func g() string{ //@codeactionedit("func", "refactor.extract", renamed_import)
+-    return time1.Now().string()
+-}
+-