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()
+-}
+-