| // Copyright 2020 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 source |
| |
| import ( |
| "bytes" |
| "context" |
| "fmt" |
| "go/ast" |
| "go/format" |
| "go/token" |
| "go/types" |
| |
| "golang.org/x/tools/go/ast/astutil" |
| "golang.org/x/tools/internal/analysisinternal" |
| "golang.org/x/tools/internal/lsp/protocol" |
| "golang.org/x/tools/internal/span" |
| ) |
| |
| func ExtractVariable(ctx context.Context, snapshot Snapshot, fh FileHandle, protoRng protocol.Range) ([]protocol.TextEdit, error) { |
| pkg, pgh, err := getParsedFile(ctx, snapshot, fh, NarrowestPackageHandle) |
| if err != nil { |
| return nil, fmt.Errorf("ExtractVariable: %v", err) |
| } |
| file, _, m, _, err := pgh.Cached() |
| if err != nil { |
| return nil, err |
| } |
| spn, err := m.RangeSpan(protoRng) |
| if err != nil { |
| return nil, err |
| } |
| rng, err := spn.Range(m.Converter) |
| if err != nil { |
| return nil, err |
| } |
| path, _ := astutil.PathEnclosingInterval(file, rng.Start, rng.End) |
| if len(path) == 0 { |
| return nil, nil |
| } |
| fset := snapshot.View().Session().Cache().FileSet() |
| node := path[0] |
| tok := fset.File(node.Pos()) |
| if tok == nil { |
| return nil, fmt.Errorf("ExtractVariable: no token.File for %s", fh.URI()) |
| } |
| var content []byte |
| if content, err = fh.Read(); err != nil { |
| return nil, err |
| } |
| if rng.Start != node.Pos() || rng.End != node.End() { |
| return nil, nil |
| } |
| |
| // Adjust new variable name until no collisons in scope. |
| scopes := collectScopes(pkg, path, node.Pos()) |
| name := "x0" |
| idx := 0 |
| for !isValidName(name, scopes) { |
| idx++ |
| name = fmt.Sprintf("x%d", idx) |
| } |
| |
| var assignment string |
| expr, ok := node.(ast.Expr) |
| if !ok { |
| return nil, nil |
| } |
| // Create new AST node for extracted code |
| switch expr.(type) { |
| case *ast.BasicLit, *ast.CompositeLit, *ast.IndexExpr, |
| *ast.SliceExpr, *ast.UnaryExpr, *ast.BinaryExpr, *ast.SelectorExpr: // TODO: stricter rules for selectorExpr |
| assignStmt := &ast.AssignStmt{ |
| Lhs: []ast.Expr{ast.NewIdent(name)}, |
| Tok: token.DEFINE, |
| Rhs: []ast.Expr{expr}, |
| } |
| var buf bytes.Buffer |
| if err = format.Node(&buf, fset, assignStmt); err != nil { |
| return nil, err |
| } |
| assignment = buf.String() |
| case *ast.CallExpr: // TODO: find number of return values and do according actions. |
| return nil, nil |
| default: |
| return nil, nil |
| } |
| |
| insertBeforeStmt := analysisinternal.StmtToInsertVarBefore(path) |
| if insertBeforeStmt == nil { |
| return nil, nil |
| } |
| |
| // Convert token.Pos to protcol.Position |
| rng = span.NewRange(fset, insertBeforeStmt.Pos(), insertBeforeStmt.End()) |
| spn, err = rng.Span() |
| if err != nil { |
| return nil, nil |
| } |
| beforeStmtStart, err := m.Position(spn.Start()) |
| if err != nil { |
| return nil, nil |
| } |
| stmtBeforeRng := protocol.Range{ |
| Start: beforeStmtStart, |
| End: beforeStmtStart, |
| } |
| |
| // Calculate indentation for insertion |
| line := tok.Line(insertBeforeStmt.Pos()) |
| lineOffset := tok.Offset(tok.LineStart(line)) |
| stmtOffset := tok.Offset(insertBeforeStmt.Pos()) |
| indent := content[lineOffset:stmtOffset] // space between these is indentation. |
| |
| return []protocol.TextEdit{ |
| { |
| Range: stmtBeforeRng, |
| NewText: assignment + "\n" + string(indent), |
| }, |
| { |
| Range: protoRng, |
| NewText: name, |
| }, |
| }, nil |
| } |
| |
| // Check for variable collision in scope. |
| func isValidName(name string, scopes []*types.Scope) bool { |
| for _, scope := range scopes { |
| if scope == nil { |
| continue |
| } |
| if scope.Lookup(name) != nil { |
| return false |
| } |
| } |
| return true |
| } |