internal/lsp: add prepare rename support

Prepare rename gets the range of the identifier to rename. Returns an
error when there is no identifier to rename.

Change-Id: I5e5865bc9ff97e6a95ac4f0c48edddcfd0f9ed67
Reviewed-on: https://go-review.googlesource.com/c/tools/+/191170
Run-TryBot: Suzy Mueller <suzmue@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Suzy Mueller 2019-08-22 13:31:03 -04:00
parent 114c575556
commit c9403068c1
11 changed files with 228 additions and 15 deletions

View File

@ -60,6 +60,10 @@ func (r *runner) Rename(t *testing.T, data tests.Renames) {
//TODO: add command line rename tests when it works
}
func (r *runner) PrepareRename(t *testing.T, data tests.PrepareRenames) {
//TODO: add command line prepare rename tests when it works
}
func (r *runner) Symbol(t *testing.T, data tests.Symbols) {
//TODO: add command line symbol tests when it works
}

View File

@ -90,6 +90,14 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara
} else {
codeActionProvider = true
}
var renameOpts interface{}
if params.Capabilities.TextDocument.Rename.PrepareSupport {
renameOpts = &protocol.RenameOptions{
PrepareProvider: true,
}
} else {
renameOpts = true
}
return &protocol.InitializeResult{
Capabilities: protocol.ServerCapabilities{
CodeActionProvider: codeActionProvider,
@ -104,7 +112,7 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara
DocumentHighlightProvider: true,
DocumentLinkProvider: &protocol.DocumentLinkOptions{},
ReferencesProvider: true,
RenameProvider: true,
RenameProvider: renameOpts,
SignatureHelpProvider: &protocol.SignatureHelpOptions{
TriggerCharacters: []string{"(", ","},
},

View File

@ -644,6 +644,50 @@ func (r *runner) Rename(t *testing.T, data tests.Renames) {
}
}
func (r *runner) PrepareRename(t *testing.T, data tests.PrepareRenames) {
for src, want := range data {
sm, err := r.mapper(src.URI())
if err != nil {
t.Fatal(err)
}
loc, err := sm.Location(src)
if err != nil {
t.Fatalf("failed for %v: %v", src, err)
}
params := &protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI},
Position: loc.Range.Start,
}
rng, err := r.server.PrepareRename(context.Background(), params)
if err != nil {
t.Errorf("prepare rename failed for %v: got error: %v", src, err)
continue
}
if rng == nil {
if want.Text != "" { // expected an ident.
t.Errorf("prepare rename failed for %v: got nil", src)
}
continue
}
wantSpn, err := want.Range.Span()
if err != nil {
t.Fatalf("failed for %v: %v", src, err)
}
got, err := sm.RangeSpan(*rng)
if err != nil {
t.Fatalf("failed for %v: %v", src, err)
}
if got != wantSpn {
t.Errorf("prepare rename failed: incorrect range got %v want %v", got, wantSpn)
}
}
}
func applyEdits(contents string, edits []diff.TextEdit) string {
res := contents

View File

@ -46,3 +46,35 @@ func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*pr
return &protocol.WorkspaceEdit{Changes: &changes}, nil
}
func (s *Server) prepareRename(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Range, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.session.ViewOf(uri)
f, err := getGoFile(ctx, view, uri)
if err != nil {
return nil, err
}
m, err := getMapper(ctx, f)
if err != nil {
return nil, err
}
// Find the identifier at the position.
ident, err := source.PrepareRename(ctx, view, f, params.Position)
if err != nil {
// Do not return the errors here, as it adds clutter.
// Returning a nil result means there is not a valid rename.
return nil, nil
}
identSpn, err := ident.Range.Span()
if err != nil {
return nil, err
}
identRng, err := m.Range(identSpn)
if err != nil {
return nil, err
}
// TODO(suzmue): return ident.Name as the placeholder text.
return &identRng, nil
}

View File

@ -268,8 +268,9 @@ func (s *Server) LogTraceNotification(context.Context, *protocol.LogTraceParams)
return notImplemented("LogtraceNotification")
}
func (s *Server) PrepareRename(context.Context, *protocol.TextDocumentPositionParams) (*protocol.Range, error) {
return nil, notImplemented("PrepareRename")
func (s *Server) PrepareRename(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Range, error) {
// TODO(suzmue): support sending placeholder text.
return s.prepareRename(ctx, params)
}
func (s *Server) SetTraceNotification(context.Context, *protocol.SetTraceParams) error {

View File

@ -313,7 +313,7 @@ func importSpec(ctx context.Context, view View, f GoFile, fAST *ast.File, pkg Pa
Name: importPath,
pkg: pkg,
}
if result.mappedRange, err = posToRange(ctx, view, imp.Pos(), imp.End()); err != nil {
if result.mappedRange, err = posToRange(ctx, view, imp.Path.Pos(), imp.Path.End()); err != nil {
return nil, err
}
// Consider the "declaration" of an import spec to be the imported package.

View File

@ -15,6 +15,7 @@ import (
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/trace"
"golang.org/x/tools/refactor/satisfy"
@ -35,6 +36,39 @@ type renamer struct {
changeMethods bool
}
type PrepareItem struct {
Range span.Range
Text string
}
func PrepareRename(ctx context.Context, view View, f GoFile, pos protocol.Position) (*PrepareItem, error) {
ctx, done := trace.StartSpan(ctx, "source.PrepareRename")
defer done()
i, err := Identifier(ctx, view, f, pos)
if err != nil {
return nil, err
}
// If the object declaration is nil, assume it is an import spec.
if i.Declaration.obj == nil {
// Find the corresponding package name for this import spec
// and rename that instead.
i, err = i.getPkgName(ctx)
if err != nil {
return nil, err
}
}
// Do not rename builtin identifiers.
if i.Declaration.obj.Parent() == types.Universe {
return nil, errors.Errorf("cannot rename builtin %q", i.Name)
}
return &PrepareItem{
Range: i.spanRange,
Text: i.Name,
}, nil
}
// Rename returns a map of TextEdits for each file modified when renaming a given identifier within a package.
func (i *IdentifierInfo) Rename(ctx context.Context, view View, newName string) (map[span.URI][]diff.TextEdit, error) {
ctx, done := trace.StartSpan(ctx, "source.Rename")
@ -53,7 +87,7 @@ func (i *IdentifierInfo) Rename(ctx context.Context, view View, newName string)
if i.Name == newName {
return nil, errors.Errorf("old and new names are the same: %s", newName)
}
if !isValidIdentifier(i.Name) {
if !isValidIdentifier(newName) {
return nil, errors.Errorf("invalid identifier to rename: %q", i.Name)
}
// Do not rename builtin identifiers.
@ -112,18 +146,31 @@ func (i *IdentifierInfo) Rename(ctx context.Context, view View, newName string)
// getPkgName gets the pkg name associated with an identifer representing
// the import path in an import spec.
func (i *IdentifierInfo) getPkgName(ctx context.Context) (*IdentifierInfo, error) {
file := i.File.FileSet().File(i.mappedRange.spanRange.Start)
pkgLine := file.Line(i.mappedRange.spanRange.Start)
file, err := i.File.GetAST(ctx, ParseHeader)
if err != nil {
return nil, err
}
var namePos token.Pos
for _, spec := range file.Imports {
if spec.Path.Pos() == i.spanRange.Start {
namePos = spec.Pos()
break
}
}
if !namePos.IsValid() {
return nil, errors.Errorf("import spec not found for %q", i.Name)
}
// Look for the object defined at NamePos.
for _, obj := range i.pkg.GetTypesInfo().Defs {
pkgName, ok := obj.(*types.PkgName)
if ok && file.Line(pkgName.Pos()) == pkgLine {
if ok && pkgName.Pos() == namePos {
return getPkgNameIdentifier(ctx, i, pkgName)
}
}
for _, obj := range i.pkg.GetTypesInfo().Implicits {
pkgName, ok := obj.(*types.PkgName)
if ok && file.Line(pkgName.Pos()) == pkgLine {
if ok && pkgName.Pos() == namePos {
return getPkgNameIdentifier(ctx, i, pkgName)
}
}

View File

@ -634,6 +634,53 @@ func applyEdits(contents string, edits []diff.TextEdit) string {
return res
}
func (r *runner) PrepareRename(t *testing.T, data tests.PrepareRenames) {
ctx := context.Background()
for src, want := range data {
f, err := r.view.GetFile(ctx, src.URI())
if err != nil {
t.Fatal(err)
}
srcRng, err := spanToRange(r.data, src)
if err != nil {
t.Fatal(err)
}
// Find the identifier at the position.
item, err := source.PrepareRename(ctx, r.view, f.(source.GoFile), srcRng.Start)
if err != nil {
if want.Text != "" { // expected an ident.
t.Errorf("prepare rename failed for %v: got error: %v", src, err)
}
continue
}
if item == nil {
if want.Text != "" {
t.Errorf("prepare rename failed for %v: got nil", src)
}
continue
}
if want.Text == "" && item != nil {
t.Errorf("prepare rename failed for %v: expected nil, got %v", src, item)
continue
}
gotSpn, err := item.Range.Span()
if err != nil {
t.Fatal(err)
}
wantSpn, err := want.Range.Span()
if err != nil {
t.Fatal(err)
}
if gotSpn != wantSpn {
t.Errorf("prepare rename failed: incorrect range got %v want %v", item.Range, want.Range)
}
}
}
func (r *runner) Symbol(t *testing.T, data tests.Symbols) {
ctx := r.ctx
for uri, expectedSymbols := range data {

View File

@ -1,6 +1,6 @@
package good //@diag("package", "no_diagnostics", "")
func stuff() { //@item(good_stuff, "stuff", "func()", "func")
func stuff() { //@item(good_stuff, "stuff", "func()", "func"),prepare("stu", "stuff", "stuff")
x := 5
random2(x)
random2(x) //@prepare("dom", "random2", "random2")
}

View File

@ -1,17 +1,19 @@
package good //@diag("package", "no_diagnostics", "")
import (
"golang.org/x/tools/internal/lsp/types" //@item(types_import, "types", "\"golang.org/x/tools/internal/lsp/types\"", "package")
_ "go/ast" //@prepare("go/ast", "_", "_")
"golang.org/x/tools/internal/lsp/types" //@item(types_import, "types", "\"golang.org/x/tools/internal/lsp/types\"", "package"),prepare("types","\"", "types")
)
func random() int { //@item(good_random, "random", "func() int", "func")
y := 6 + 7
return y
_ = "random() int" //@prepare("random", "", "")
y := 6 + 7 //@prepare("7", "", "")
return y //@prepare("return", "","")
}
func random2(y int) int { //@item(good_random2, "random2", "func(y int) int", "func"),item(good_y_param, "y", "int", "parameter")
//@complete("", good_y_param, types_import, good_random, good_random2, good_stuff)
var b types.Bob = &types.X{}
var b types.Bob = &types.X{} //@prepare("ypes","types", "types")
if _, ok := b.(*types.X); ok { //@complete("X", X_struct, Y_struct, Bob_interface)
}

View File

@ -40,6 +40,7 @@ const (
ExpectedHighlightsCount = 2
ExpectedReferencesCount = 5
ExpectedRenamesCount = 20
ExpectedPrepareRenamesCount = 8
ExpectedSymbolsCount = 1
ExpectedSignaturesCount = 21
ExpectedLinksCount = 4
@ -65,6 +66,7 @@ type Definitions map[span.Span]Definition
type Highlights map[string][]span.Span
type References map[span.Span][]span.Span
type Renames map[span.Span]string
type PrepareRenames map[span.Span]*source.PrepareItem
type Symbols map[span.URI][]source.Symbol
type SymbolsChildren map[string][]source.Symbol
type Signatures map[span.Span]*source.SignatureInformation
@ -84,6 +86,7 @@ type Data struct {
Highlights Highlights
References References
Renames Renames
PrepareRenames PrepareRenames
Symbols Symbols
symbolsChildren SymbolsChildren
Signatures Signatures
@ -105,6 +108,7 @@ type Tests interface {
Highlight(*testing.T, Highlights)
Reference(*testing.T, References)
Rename(*testing.T, Renames)
PrepareRename(*testing.T, PrepareRenames)
Symbol(*testing.T, Symbols)
SignatureHelp(*testing.T, Signatures)
Link(*testing.T, Links)
@ -151,6 +155,7 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
Highlights: make(Highlights),
References: make(References),
Renames: make(Renames),
PrepareRenames: make(PrepareRenames),
Symbols: make(Symbols),
symbolsChildren: make(SymbolsChildren),
Signatures: make(Signatures),
@ -235,6 +240,7 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
"highlight": data.collectHighlights,
"refs": data.collectReferences,
"rename": data.collectRenames,
"prepare": data.collectPrepareRenames,
"symbol": data.collectSymbols,
"signature": data.collectSignatures,
"snippet": data.collectCompletionSnippets,
@ -339,6 +345,15 @@ func Run(t *testing.T, tests Tests, data *Data) {
tests.Rename(t, data.Renames)
})
t.Run("PrepareRenames", func(t *testing.T) {
t.Helper()
if len(data.PrepareRenames) != ExpectedPrepareRenamesCount {
t.Errorf("got %v prepare renames expected %v", len(data.PrepareRenames), ExpectedPrepareRenamesCount)
}
tests.PrepareRename(t, data.PrepareRenames)
})
t.Run("Symbols", func(t *testing.T) {
t.Helper()
if len(data.Symbols) != ExpectedSymbolsCount {
@ -613,6 +628,19 @@ func (data *Data) collectRenames(src span.Span, newText string) {
data.Renames[src] = newText
}
func (data *Data) collectPrepareRenames(src span.Span, rng span.Range, placeholder string) {
if int(rng.End-rng.Start) != len(placeholder) {
// If the length of the placeholder and the length of the range do not match,
// make the range just be the start.
rng = span.NewRange(rng.FileSet, rng.Start, rng.Start)
}
data.PrepareRenames[src] = &source.PrepareItem{
Range: rng,
Text: placeholder,
}
}
func (data *Data) collectSymbols(name string, spn span.Span, kind string, parentName string) {
sym := source.Symbol{
Name: name,