diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go index a1a0e9d2..06f39b73 100644 --- a/internal/lsp/diagnostics.go +++ b/internal/lsp/diagnostics.go @@ -5,6 +5,7 @@ package lsp import ( + "fmt" "go/token" "strconv" "strings" @@ -19,6 +20,9 @@ func diagnostics(v *source.View, uri source.URI) (map[string][]protocol.Diagnost if err != nil { return nil, err } + if pkg == nil { + return nil, fmt.Errorf("package for %v not found", uri) + } reports := make(map[string][]protocol.Diagnostic) for _, filename := range pkg.GoFiles { reports[filename] = []protocol.Diagnostic{} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index c07748ec..9ee806ae 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -51,6 +51,7 @@ func (s *server) Initialize(ctx context.Context, params *protocol.InitializePara CompletionProvider: protocol.CompletionOptions{ TriggerCharacters: []string{"."}, }, + DefinitionProvider: true, }, }, nil } @@ -170,8 +171,18 @@ func (s *server) SignatureHelp(context.Context, *protocol.TextDocumentPositionPa return nil, notImplemented("SignatureHelp") } -func (s *server) Definition(context.Context, *protocol.TextDocumentPositionParams) ([]protocol.Location, error) { - return nil, notImplemented("Definition") +func (s *server) Definition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) { + f := s.view.GetFile(source.URI(params.TextDocument.URI)) + tok, err := f.GetToken() + if err != nil { + return nil, err + } + pos := fromProtocolPosition(tok, params.Position) + r, err := source.Definition(ctx, f, pos) + if err != nil { + return nil, err + } + return []protocol.Location{toProtocolLocation(s.view, r)}, nil } func (s *server) TypeDefinition(context.Context, *protocol.TextDocumentPositionParams) ([]protocol.Location, error) { diff --git a/internal/lsp/source/definition.go b/internal/lsp/source/definition.go new file mode 100644 index 00000000..9886f3d4 --- /dev/null +++ b/internal/lsp/source/definition.go @@ -0,0 +1,66 @@ +// Copyright 2018 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 ( + "context" + "fmt" + "go/ast" + "go/token" + + "golang.org/x/tools/go/ast/astutil" +) + +func Definition(ctx context.Context, f *File, pos token.Pos) (Range, error) { + fAST, err := f.GetAST() + if err != nil { + return Range{}, err + } + pkg, err := f.GetPackage() + if err != nil { + return Range{}, err + } + ident, err := findIdentifier(fAST, pos) + if err != nil { + return Range{}, err + } + if ident == nil { + return Range{}, fmt.Errorf("definition was not a valid identifier") + } + obj := pkg.TypesInfo.ObjectOf(ident) + if obj == nil { + return Range{}, fmt.Errorf("no object") + } + return Range{ + Start: obj.Pos(), + End: obj.Pos() + token.Pos(len([]byte(obj.Name()))), // TODO: use real range of obj + }, nil +} + +// findIdentifier returns the ast.Ident for a position +// in a file, accounting for a potentially incomplete selector. +func findIdentifier(f *ast.File, pos token.Pos) (*ast.Ident, error) { + path, _ := astutil.PathEnclosingInterval(f, pos, pos) + if path == nil { + return nil, fmt.Errorf("can't find node enclosing position") + } + // If the position is not an identifier but immediately follows + // an identifier or selector period (as is common when + // requesting a completion), use the path to the preceding node. + if ident, ok := path[0].(*ast.Ident); ok { + return ident, nil + } + path, _ = astutil.PathEnclosingInterval(f, pos-1, pos-1) + if path == nil { + return nil, nil + } + switch prev := path[0].(type) { + case *ast.Ident: + return prev, nil + case *ast.SelectorExpr: + return prev.Sel, nil + } + return nil, nil +}