blob: fb53e61727ebd8cc8e26c15cfb5d8f4c90fec632 [file] [log] [blame]
// 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
}