gopls/internal/lsp: separate some requests from source.Identifier
Start to unwind source.Identifier by unrolling definition, type
definition, and call hierarchy handlers.
Along the way, introduce a couple primitive helper functions, which may
be made obsolete in the future but allowed preserving source.Identifier
behavior:
- referencedObject returns the object referenced by the cursor position,
as defined by source.Identifier.
- mapPosition is a helper to map token.Pos to MappedRange in the narrow
context of a package fileset.
After this change, the only remaining use of source.Identifier is for
Hover, but that is a sizeable refactoring and therefore left to a
subsequent CL.
Updates golang/go#57987
Change-Id: Iba4b0a574e6a6d3d54253f3b4bff8fe6e13a1b15
Reviewed-on: https://go-review.googlesource.com/c/tools/+/463955
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/internal/lsp/definition.go b/gopls/internal/lsp/definition.go
index cd91a04..cea57c3 100644
--- a/gopls/internal/lsp/definition.go
+++ b/gopls/internal/lsp/definition.go
@@ -14,41 +14,33 @@
)
func (s *Server) definition(ctx context.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) {
+ // TODO(rfindley): definition requests should be multiplexed across all views.
snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
defer release()
if !ok {
return nil, err
}
- if snapshot.View().FileKind(fh) == source.Tmpl {
+ switch kind := snapshot.View().FileKind(fh); kind {
+ case source.Tmpl:
return template.Definition(snapshot, fh, params.Position)
+ case source.Go:
+ return source.Definition(ctx, snapshot, fh, params.Position)
+ default:
+ return nil, fmt.Errorf("can't find definitions for file type %s", kind)
}
- ident, err := source.Identifier(ctx, snapshot, fh, params.Position)
- if err != nil {
- return nil, err
- }
- if ident.IsImport() && !snapshot.View().Options().ImportShortcut.ShowDefinition() {
- return nil, nil
- }
- var locations []protocol.Location
- for _, ref := range ident.Declaration.MappedRange {
- locations = append(locations, ref.Location())
- }
-
- return locations, nil
}
func (s *Server) typeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) ([]protocol.Location, error) {
+ // TODO(rfindley): type definition requests should be multiplexed across all views.
snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
defer release()
if !ok {
return nil, err
}
- ident, err := source.Identifier(ctx, snapshot, fh, params.Position)
- if err != nil {
- return nil, err
+ switch kind := snapshot.View().FileKind(fh); kind {
+ case source.Go:
+ return source.TypeDefinition(ctx, snapshot, fh, params.Position)
+ default:
+ return nil, fmt.Errorf("can't find type definitions for file type %s", kind)
}
- if ident.Type.Object == nil {
- return nil, fmt.Errorf("no type definition for %s", ident.Name)
- }
- return []protocol.Location{ident.Type.MappedRange.Location()}, nil
}
diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go
index 47f6b0a..e648828 100644
--- a/gopls/internal/lsp/lsp_test.go
+++ b/gopls/internal/lsp/lsp_test.go
@@ -666,7 +666,9 @@
}
}
-func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) {
+// TODO(rfindley): This handler needs more work. The output is still a bit hard
+// to read (range diffs do not format nicely), and it is too entangled with hover.
+func (r *runner) Definition(t *testing.T, _ span.Span, d tests.Definition) {
sm, err := r.data.Mapper(d.Src.URI())
if err != nil {
t.Fatal(err)
@@ -676,18 +678,18 @@
t.Fatalf("failed for %v: %v", d.Src, err)
}
tdpp := protocol.LocationTextDocumentPositionParams(loc)
- var locs []protocol.Location
+ var got []protocol.Location
var hover *protocol.Hover
if d.IsType {
params := &protocol.TypeDefinitionParams{
TextDocumentPositionParams: tdpp,
}
- locs, err = r.server.TypeDefinition(r.ctx, params)
+ got, err = r.server.TypeDefinition(r.ctx, params)
} else {
params := &protocol.DefinitionParams{
TextDocumentPositionParams: tdpp,
}
- locs, err = r.server.Definition(r.ctx, params)
+ got, err = r.server.Definition(r.ctx, params)
if err != nil {
t.Fatalf("failed for %v: %+v", d.Src, err)
}
@@ -699,8 +701,19 @@
if err != nil {
t.Fatalf("failed for %v: %v", d.Src, err)
}
- if len(locs) != 1 {
- t.Errorf("got %d locations for definition, expected 1", len(locs))
+ dm, err := r.data.Mapper(d.Def.URI())
+ if err != nil {
+ t.Fatal(err)
+ }
+ def, err := dm.SpanLocation(d.Def)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !d.OnlyHover {
+ want := []protocol.Location{def}
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Fatalf("Definition(%s) mismatch (-want +got):\n%s", d.Src, diff)
+ }
}
didSomething := false
if hover != nil {
@@ -717,13 +730,13 @@
}
if !d.OnlyHover {
didSomething = true
- locURI := locs[0].URI.SpanURI()
+ locURI := got[0].URI.SpanURI()
lm, err := r.data.Mapper(locURI)
if err != nil {
t.Fatal(err)
}
- if def, err := lm.LocationSpan(locs[0]); err != nil {
- t.Fatalf("failed for %v: %v", locs[0], err)
+ if def, err := lm.LocationSpan(got[0]); err != nil {
+ t.Fatalf("failed for %v: %v", got[0], err)
} else if def != d.Def {
t.Errorf("for %v got %v want %v", d.Src, def, d.Def)
}
diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go
index cefc539..31fdf49 100644
--- a/gopls/internal/lsp/source/call_hierarchy.go
+++ b/gopls/internal/lsp/source/call_hierarchy.go
@@ -15,44 +15,48 @@
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/safetoken"
+ "golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/internal/bug"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
)
// PrepareCallHierarchy returns an array of CallHierarchyItem for a file and the position within the file.
-func PrepareCallHierarchy(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyItem, error) {
+func PrepareCallHierarchy(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) ([]protocol.CallHierarchyItem, error) {
ctx, done := event.Start(ctx, "source.PrepareCallHierarchy")
defer done()
- identifier, err := Identifier(ctx, snapshot, fh, pos)
+ pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage)
if err != nil {
- if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) {
- return nil, nil
- }
+ return nil, err
+ }
+ pos, err := pgf.PositionPos(pp)
+ if err != nil {
return nil, err
}
- // The identifier can be nil if it is an import spec.
- if identifier == nil || identifier.Declaration.obj == nil {
+ obj := referencedObject(pkg, pgf, pos)
+ if obj == nil {
return nil, nil
}
- if _, ok := identifier.Declaration.obj.Type().Underlying().(*types.Signature); !ok {
+ if _, ok := obj.Type().Underlying().(*types.Signature); !ok {
return nil, nil
}
- if len(identifier.Declaration.MappedRange) == 0 {
- return nil, nil
+ declLoc, err := mapPosition(ctx, pkg.FileSet(), snapshot, obj.Pos(), adjustedObjEnd(obj))
+ if err != nil {
+ return nil, err
}
- declMappedRange := identifier.Declaration.MappedRange[0]
- rng := declMappedRange.Range()
+ rng := declLoc.Range
callHierarchyItem := protocol.CallHierarchyItem{
- Name: identifier.Name,
+ Name: obj.Name(),
Kind: protocol.Function,
Tags: []protocol.SymbolTag{},
- Detail: fmt.Sprintf("%s • %s", identifier.Declaration.obj.Pkg().Path(), filepath.Base(declMappedRange.URI().Filename())),
- URI: protocol.DocumentURI(declMappedRange.URI()),
+ Detail: fmt.Sprintf("%s • %s", obj.Pkg().Path(), filepath.Base(declLoc.URI.SpanURI().Filename())),
+ URI: declLoc.URI,
Range: rng,
SelectionRange: rng,
}
@@ -174,41 +178,71 @@
}
// OutgoingCalls returns an array of CallHierarchyOutgoingCall for a file and the position within the file.
-func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyOutgoingCall, error) {
+func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) ([]protocol.CallHierarchyOutgoingCall, error) {
ctx, done := event.Start(ctx, "source.OutgoingCalls")
defer done()
- identifier, err := Identifier(ctx, snapshot, fh, pos)
+ pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage)
if err != nil {
- if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) {
- return nil, nil
- }
return nil, err
}
-
- if _, ok := identifier.Declaration.obj.Type().Underlying().(*types.Signature); !ok {
- return nil, nil
- }
- node := identifier.Declaration.node
- if node == nil {
- return nil, nil
- }
- callExprs, err := collectCallExpressions(identifier.Declaration.nodeFile, node)
+ pos, err := pgf.PositionPos(pp)
if err != nil {
return nil, err
}
- return toProtocolOutgoingCalls(ctx, snapshot, fh, callExprs)
-}
+ obj := referencedObject(pkg, pgf, pos)
+ if obj == nil {
+ return nil, nil
+ }
-// collectCallExpressions collects call expression ranges inside a function.
-func collectCallExpressions(pgf *ParsedGoFile, node ast.Node) ([]protocol.Range, error) {
- type callPos struct {
+ if _, ok := obj.Type().Underlying().(*types.Signature); !ok {
+ return nil, nil
+ }
+
+ // Skip builtins.
+ if obj.Pkg() == nil {
+ return nil, nil
+ }
+
+ if !obj.Pos().IsValid() {
+ return nil, bug.Errorf("internal error: object %s.%s missing position", obj.Pkg().Path(), obj.Name())
+ }
+
+ declFile := pkg.FileSet().File(obj.Pos())
+ if declFile == nil {
+ return nil, bug.Errorf("file not found for %d", obj.Pos())
+ }
+
+ uri := span.URIFromPath(declFile.Name())
+ offset, err := safetoken.Offset(declFile, obj.Pos())
+ if err != nil {
+ return nil, err
+ }
+
+ // Use TypecheckFull as we want to inspect the body of the function declaration.
+ declPkg, declPGF, err := PackageForFile(ctx, snapshot, uri, TypecheckFull, NarrowestPackage)
+ if err != nil {
+ return nil, err
+ }
+
+ declPos, err := safetoken.Pos(declPGF.Tok, offset)
+ if err != nil {
+ return nil, err
+ }
+
+ declNode, _ := FindDeclAndField([]*ast.File{declPGF.File}, declPos)
+ if declNode == nil {
+ // TODO(rfindley): why don't we return an error here, or even bug.Errorf?
+ return nil, nil
+ // return nil, bug.Errorf("failed to find declaration for object %s.%s", obj.Pkg().Path(), obj.Name())
+ }
+
+ type callRange struct {
start, end token.Pos
}
- callPositions := []callPos{}
-
- ast.Inspect(node, func(n ast.Node) bool {
+ callRanges := []callRange{}
+ ast.Inspect(declNode, func(n ast.Node) bool {
if call, ok := n.(*ast.CallExpr); ok {
var start, end token.Pos
switch n := call.Fun.(type) {
@@ -225,70 +259,48 @@
// for ex: direct function literal calls since that's not an 'outgoing' call
return false
}
- callPositions = append(callPositions, callPos{start: start, end: end})
+ callRanges = append(callRanges, callRange{start: start, end: end})
}
return true
})
- callRanges := []protocol.Range{}
- for _, call := range callPositions {
- callRange, err := pgf.PosRange(call.start, call.end)
- if err != nil {
- return nil, err
- }
- callRanges = append(callRanges, callRange)
- }
- return callRanges, nil
-}
-
-// toProtocolOutgoingCalls returns an array of protocol.CallHierarchyOutgoingCall for ast call expressions.
-// Calls to the same function are assigned to the same declaration.
-func toProtocolOutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, callRanges []protocol.Range) ([]protocol.CallHierarchyOutgoingCall, error) {
- // Multiple calls could be made to the same function, defined by "same declaration
- // AST node & same identifier name" to provide a unique identifier key even when
- // the func is declared in a struct or interface.
- type key struct {
- decl ast.Node
- name string
- }
- outgoingCalls := map[key]*protocol.CallHierarchyOutgoingCall{}
+ outgoingCalls := map[token.Pos]*protocol.CallHierarchyOutgoingCall{}
for _, callRange := range callRanges {
- identifier, err := Identifier(ctx, snapshot, fh, callRange.Start)
- if err != nil {
- if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) {
- continue
- }
- return nil, err
+ obj := referencedObject(declPkg, declPGF, callRange.start)
+ if obj == nil {
+ continue
}
// ignore calls to builtin functions
- if identifier.Declaration.obj.Pkg() == nil {
+ if obj.Pkg() == nil {
continue
}
- if outgoingCall, ok := outgoingCalls[key{identifier.Declaration.node, identifier.Name}]; ok {
- outgoingCall.FromRanges = append(outgoingCall.FromRanges, callRange)
- continue
+ outgoingCall, ok := outgoingCalls[obj.Pos()]
+ if !ok {
+ loc, err := mapPosition(ctx, declPkg.FileSet(), snapshot, obj.Pos(), obj.Pos()+token.Pos(len(obj.Name())))
+ if err != nil {
+ return nil, err
+ }
+ outgoingCall = &protocol.CallHierarchyOutgoingCall{
+ To: protocol.CallHierarchyItem{
+ Name: obj.Name(),
+ Kind: protocol.Function,
+ Tags: []protocol.SymbolTag{},
+ Detail: fmt.Sprintf("%s • %s", obj.Pkg().Path(), filepath.Base(loc.URI.SpanURI().Filename())),
+ URI: loc.URI,
+ Range: loc.Range,
+ SelectionRange: loc.Range,
+ },
+ }
+ outgoingCalls[obj.Pos()] = outgoingCall
}
- if len(identifier.Declaration.MappedRange) == 0 {
- continue
+ rng, err := declPGF.PosRange(callRange.start, callRange.end)
+ if err != nil {
+ return nil, err
}
- declMappedRange := identifier.Declaration.MappedRange[0]
- rng := declMappedRange.Range()
-
- outgoingCalls[key{identifier.Declaration.node, identifier.Name}] = &protocol.CallHierarchyOutgoingCall{
- To: protocol.CallHierarchyItem{
- Name: identifier.Name,
- Kind: protocol.Function,
- Tags: []protocol.SymbolTag{},
- Detail: fmt.Sprintf("%s • %s", identifier.Declaration.obj.Pkg().Path(), filepath.Base(declMappedRange.URI().Filename())),
- URI: protocol.DocumentURI(declMappedRange.URI()),
- Range: rng,
- SelectionRange: rng,
- },
- FromRanges: []protocol.Range{callRange},
- }
+ outgoingCall.FromRanges = append(outgoingCall.FromRanges, rng)
}
outgoingCallItems := make([]protocol.CallHierarchyOutgoingCall, 0, len(outgoingCalls))
diff --git a/gopls/internal/lsp/source/definition.go b/gopls/internal/lsp/source/definition.go
new file mode 100644
index 0000000..d9dd446
--- /dev/null
+++ b/gopls/internal/lsp/source/definition.go
@@ -0,0 +1,220 @@
+// Copyright 2023 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"
+ "go/types"
+
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/internal/bug"
+ "golang.org/x/tools/internal/event"
+)
+
+// Definition handles the textDocument/definition request for Go files.
+func Definition(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) ([]protocol.Location, error) {
+ ctx, done := event.Start(ctx, "source.Definition")
+ defer done()
+
+ pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage)
+ if err != nil {
+ return nil, err
+ }
+ pos, err := pgf.PositionPos(position)
+ if err != nil {
+ return nil, err
+ }
+
+ // Handle the case where the cursor is in an import.
+ importLocations, err := importDefinition(ctx, snapshot, pkg, pgf, pos)
+ if err != nil {
+ return nil, err
+ }
+ if len(importLocations) > 0 {
+ return importLocations, nil
+ }
+
+ // Handle the case where the cursor is in the package name.
+ // We use "<= End" to accept a query immediately after the package name.
+ if pgf.File != nil && pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End() {
+ // If there's no package documentation, just use current file.
+ declFile := pgf
+ for _, pgf := range pkg.CompiledGoFiles() {
+ if pgf.File.Name != nil && pgf.File.Doc != nil {
+ declFile = pgf
+ break
+ }
+ }
+ loc, err := declFile.NodeLocation(declFile.File.Name)
+ if err != nil {
+ return nil, err
+ }
+ return []protocol.Location{loc}, nil
+ }
+
+ // The general case: the cursor is on an identifier.
+ obj := referencedObject(pkg, pgf, pos)
+ if obj == nil {
+ return nil, nil
+ }
+
+ // Handle built-in identifiers.
+ if obj.Parent() == types.Universe {
+ builtin, err := snapshot.BuiltinFile(ctx)
+ if err != nil {
+ return nil, err
+ }
+ // Note that builtinObj is an ast.Object, not types.Object :)
+ builtinObj := builtin.File.Scope.Lookup(obj.Name())
+ if builtinObj == nil {
+ // Every builtin should have documentation.
+ return nil, bug.Errorf("internal error: no builtin object for %s", obj.Name())
+ }
+ decl, ok := builtinObj.Decl.(ast.Node)
+ if !ok {
+ return nil, bug.Errorf("internal error: no declaration for %s", obj.Name())
+ }
+ // The builtin package isn't in the dependency graph, so the usual
+ // utilities won't work here.
+ loc, err := builtin.PosLocation(decl.Pos(), decl.Pos()+token.Pos(len(obj.Name())))
+ if err != nil {
+ return nil, err
+ }
+ return []protocol.Location{loc}, nil
+ }
+
+ // Finally, map the object position.
+ var locs []protocol.Location
+ if !obj.Pos().IsValid() {
+ return nil, bug.Errorf("internal error: no position for %v", obj.Name())
+ }
+ loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, obj.Pos(), adjustedObjEnd(obj))
+ if err != nil {
+ return nil, err
+ }
+ locs = append(locs, loc)
+ return locs, nil
+}
+
+// referencedObject returns the object referenced at the specified position,
+// which must be within the file pgf, for the purposes of definition/hover/call
+// hierarchy operations. It may return nil if no object was found at the given
+// position.
+//
+// It differs from types.Info.ObjectOf in several ways:
+// - It adjusts positions to do a better job of finding associated
+// identifiers. For example it finds 'foo' from the cursor position _*foo
+// - It handles type switch implicits, choosing the first one.
+// - For embedded fields, it returns the type name object rather than the var
+// (field) object.
+//
+// TODO(rfindley): this function exists to preserve the pre-existing behavior
+// of source.Identifier. Eliminate this helper in favor of sharing
+// functionality with objectsAt, after choosing suitable primitives.
+func referencedObject(pkg Package, pgf *ParsedGoFile, pos token.Pos) types.Object {
+ path := pathEnclosingObjNode(pgf.File, pos)
+ if len(path) == 0 {
+ return nil
+ }
+ var obj types.Object
+ info := pkg.GetTypesInfo()
+ switch n := path[0].(type) {
+ case *ast.Ident:
+ // If leaf represents an implicit type switch object or the type
+ // switch "assign" variable, expand to all of the type switch's
+ // implicit objects.
+ if implicits, _ := typeSwitchImplicits(info, path); len(implicits) > 0 {
+ obj = implicits[0]
+ } else {
+ obj = info.ObjectOf(n)
+ }
+ // If the original position was an embedded field, we want to jump
+ // to the field's type definition, not the field's definition.
+ if v, ok := obj.(*types.Var); ok && v.Embedded() {
+ // types.Info.Uses contains the embedded field's *types.TypeName.
+ if typeName := info.Uses[n]; typeName != nil {
+ obj = typeName
+ }
+ }
+ }
+ return obj
+}
+
+// importDefinition returns locations defining a package referenced by the
+// import spec containing pos.
+//
+// If pos is not inside an import spec, it returns nil, nil.
+func importDefinition(ctx context.Context, s Snapshot, pkg Package, pgf *ParsedGoFile, pos token.Pos) ([]protocol.Location, error) {
+ var imp *ast.ImportSpec
+ for _, spec := range pgf.File.Imports {
+ // We use "<= End" to accept a query immediately after an ImportSpec.
+ if spec.Path.Pos() <= pos && pos <= spec.Path.End() {
+ imp = spec
+ }
+ }
+ if imp == nil {
+ return nil, nil
+ }
+
+ importPath := UnquoteImportPath(imp)
+ impID := pkg.Metadata().DepsByImpPath[importPath]
+ if impID == "" {
+ return nil, fmt.Errorf("failed to resolve import %q", importPath)
+ }
+ impMetadata := s.Metadata(impID)
+ if impMetadata == nil {
+ return nil, fmt.Errorf("missing information for package %q", impID)
+ }
+
+ var locs []protocol.Location
+ for _, f := range impMetadata.CompiledGoFiles {
+ fh, err := s.GetFile(ctx, f)
+ if err != nil {
+ if ctx.Err() != nil {
+ return nil, ctx.Err()
+ }
+ continue
+ }
+ pgf, err := s.ParseGo(ctx, fh, ParseHeader)
+ if err != nil {
+ if ctx.Err() != nil {
+ return nil, ctx.Err()
+ }
+ continue
+ }
+ loc, err := pgf.NodeLocation(pgf.File)
+ if err != nil {
+ return nil, err
+ }
+ locs = append(locs, loc)
+ }
+
+ if len(locs) == 0 {
+ return nil, fmt.Errorf("package %q has no readable files", impID) // incl. unsafe
+ }
+
+ return locs, nil
+}
+
+// TODO(rfindley): avoid the duplicate column mapping here, by associating a
+// column mapper with each file handle.
+func mapPosition(ctx context.Context, fset *token.FileSet, s FileSource, start, end token.Pos) (protocol.Location, error) {
+ file := fset.File(start)
+ uri := span.URIFromPath(file.Name())
+ fh, err := s.GetFile(ctx, uri)
+ if err != nil {
+ return protocol.Location{}, err
+ }
+ content, err := fh.Read()
+ if err != nil {
+ return protocol.Location{}, err
+ }
+ m := protocol.NewMapper(fh.URI(), content)
+ return m.PosLocation(file, start, end)
+}
diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go
index 0ed56e4..9497c65 100644
--- a/gopls/internal/lsp/source/identifier.go
+++ b/gopls/internal/lsp/source/identifier.go
@@ -30,7 +30,7 @@
Type struct {
MappedRange protocol.MappedRange
- Object types.Object
+ Object *types.TypeName
}
Inferred *types.Signature
@@ -48,11 +48,6 @@
qf types.Qualifier
}
-func (i *IdentifierInfo) IsImport() bool {
- _, ok := i.Declaration.node.(*ast.ImportSpec)
- return ok
-}
-
type Declaration struct {
MappedRange []protocol.MappedRange
@@ -266,7 +261,8 @@
// findFileInDeps, which is also called below. Refactor
// objToMappedRange to separate the find-file from the
// lookup-position steps to avoid the redundancy.
- rng, err := objToMappedRange(ctx, snapshot, pkg, result.Declaration.obj)
+ obj := result.Declaration.obj
+ rng, err := posToMappedRange(ctx, snapshot, pkg, obj.Pos(), adjustedObjEnd(obj))
if err != nil {
return nil, err
}
@@ -301,7 +297,9 @@
if hasErrorType(result.Type.Object) {
return result, nil
}
- if result.Type.MappedRange, err = objToMappedRange(ctx, snapshot, pkg, result.Type.Object); err != nil {
+ obj := result.Type.Object
+ // TODO(rfindley): no need to use an adjusted end here.
+ if result.Type.MappedRange, err = posToMappedRange(ctx, snapshot, pkg, obj.Pos(), adjustedObjEnd(obj)); err != nil {
return nil, err
}
}
@@ -407,7 +405,10 @@
return nil
}
-func typeToObject(typ types.Type) types.Object {
+// typeToObject returns the relevant type name for the given type, after
+// unwrapping pointers, arrays, slices, channels, and function signatures with
+// a single non-error result.
+func typeToObject(typ types.Type) *types.TypeName {
switch typ := typ.(type) {
case *types.Named:
return typ.Obj()
@@ -422,7 +423,7 @@
case *types.Signature:
// Try to find a return value of a named type. If there's only one
// such value, jump to its type definition.
- var res types.Object
+ var res *types.TypeName
results := typ.Results()
for i := 0; i < results.Len(); i++ {
diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go
index bb0b5ec..aaacc59 100644
--- a/gopls/internal/lsp/source/options.go
+++ b/gopls/internal/lsp/source/options.go
@@ -140,7 +140,7 @@
LinksInHover: true,
},
NavigationOptions: NavigationOptions{
- ImportShortcut: Both,
+ ImportShortcut: BothShortcuts,
SymbolMatcher: SymbolFastFuzzy,
SymbolStyle: DynamicSymbols,
},
@@ -596,17 +596,17 @@
type ImportShortcut string
const (
- Both ImportShortcut = "Both"
- Link ImportShortcut = "Link"
- Definition ImportShortcut = "Definition"
+ BothShortcuts ImportShortcut = "Both"
+ LinkShortcut ImportShortcut = "Link"
+ DefinitionShortcut ImportShortcut = "Definition"
)
func (s ImportShortcut) ShowLinks() bool {
- return s == Both || s == Link
+ return s == BothShortcuts || s == LinkShortcut
}
func (s ImportShortcut) ShowDefinition() bool {
- return s == Both || s == Definition
+ return s == BothShortcuts || s == DefinitionShortcut
}
type Matcher string
@@ -985,7 +985,7 @@
result.setBool(&o.LinksInHover)
case "importShortcut":
- if s, ok := result.asOneOf(string(Both), string(Link), string(Definition)); ok {
+ if s, ok := result.asOneOf(string(BothShortcuts), string(LinkShortcut), string(DefinitionShortcut)); ok {
o.ImportShortcut = ImportShortcut(s)
}
diff --git a/gopls/internal/lsp/source/type_definition.go b/gopls/internal/lsp/source/type_definition.go
new file mode 100644
index 0000000..2a54fdf
--- /dev/null
+++ b/gopls/internal/lsp/source/type_definition.go
@@ -0,0 +1,52 @@
+// Copyright 2023 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/token"
+
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/internal/event"
+)
+
+// TypeDefinition handles the textDocument/typeDefinition request for Go files.
+func TypeDefinition(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) ([]protocol.Location, error) {
+ ctx, done := event.Start(ctx, "source.TypeDefinition")
+ defer done()
+
+ pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage)
+ if err != nil {
+ return nil, err
+ }
+ pos, err := pgf.PositionPos(position)
+ if err != nil {
+ return nil, err
+ }
+
+ obj := referencedObject(pkg, pgf, pos)
+ if obj == nil {
+ return nil, nil
+ }
+
+ typObj := typeToObject(obj.Type())
+ if typObj == nil {
+ return nil, fmt.Errorf("no type definition for %s", obj.Name())
+ }
+
+ // Identifiers with the type "error" are a special case with no position.
+ if hasErrorType(typObj) {
+ // TODO(rfindley): we can do better here, returning a link to the builtin
+ // file.
+ return nil, nil
+ }
+
+ loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, typObj.Pos(), typObj.Pos()+token.Pos(len(typObj.Name())))
+ if err != nil {
+ return nil, err
+ }
+ return []protocol.Location{loc}, nil
+}
diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go
index 7dab47b..c9a5102 100644
--- a/gopls/internal/lsp/source/util.go
+++ b/gopls/internal/lsp/source/util.go
@@ -51,7 +51,12 @@
return false
}
-func objToMappedRange(ctx context.Context, snapshot Snapshot, pkg Package, obj types.Object) (protocol.MappedRange, error) {
+// adjustedObjEnd returns the end position of obj, possibly modified for
+// package names.
+//
+// TODO(rfindley): eliminate this function, by inlining it at callsites where
+// it makes sense.
+func adjustedObjEnd(obj types.Object) token.Pos {
nameLen := len(obj.Name())
if pkgName, ok := obj.(*types.PkgName); ok {
// An imported Go package has a package-local, unqualified name.
@@ -68,7 +73,7 @@
nameLen = len(pkgName.Imported().Path()) + len(`""`)
}
}
- return posToMappedRange(ctx, snapshot, pkg, obj.Pos(), obj.Pos()+token.Pos(nameLen))
+ return obj.Pos() + token.Pos(nameLen)
}
// posToMappedRange returns the MappedRange for the given [start, end) span,
@@ -133,23 +138,6 @@
}
}
-func (k FileKind) String() string {
- switch k {
- case Go:
- return "go"
- case Mod:
- return "go.mod"
- case Sum:
- return "go.sum"
- case Tmpl:
- return "tmpl"
- case Work:
- return "go.work"
- default:
- return fmt.Sprintf("unk%d", k)
- }
-}
-
// nodeAtPos returns the index and the node whose position is contained inside
// the node list.
func nodeAtPos(nodes []ast.Node, pos token.Pos) (ast.Node, int) {
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index cbcc9b4..5f4e3f5 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -697,6 +697,23 @@
Work
)
+func (k FileKind) String() string {
+ switch k {
+ case Go:
+ return "go"
+ case Mod:
+ return "go.mod"
+ case Sum:
+ return "go.sum"
+ case Tmpl:
+ return "tmpl"
+ case Work:
+ return "go.work"
+ default:
+ return fmt.Sprintf("internal error: unknown file kind %d", k)
+ }
+}
+
// Analyzer represents a go/analysis analyzer with some boolean properties
// that let the user know how to use the analyzer.
type Analyzer struct {
diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go
index cad69c1..70a3336 100644
--- a/gopls/internal/regtest/misc/definition_test.go
+++ b/gopls/internal/regtest/misc/definition_test.go
@@ -155,12 +155,11 @@
`
for _, tt := range []struct {
wantLinks int
- wantDef bool
importShortcut string
}{
- {1, false, "Link"},
- {0, true, "Definition"},
- {1, true, "Both"},
+ {1, "Link"},
+ {0, "Definition"},
+ {1, "Both"},
} {
t.Run(tt.importShortcut, func(t *testing.T) {
WithOptions(
@@ -168,9 +167,7 @@
).Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
loc := env.GoToDefinition(env.RegexpSearch("main.go", `"fmt"`))
- if !tt.wantDef && (loc != (protocol.Location{})) {
- t.Fatalf("expected no definition, got one: %v", loc)
- } else if tt.wantDef && loc == (protocol.Location{}) {
+ if loc == (protocol.Location{}) {
t.Fatalf("expected definition, got none")
}
links := env.DocumentLink("main.go")