diff --git a/gopls/internal/lsp/definition.go b/gopls/internal/lsp/definition.go index cd91a0447db..cea57c3f630 100644 --- a/gopls/internal/lsp/definition.go +++ b/gopls/internal/lsp/definition.go @@ -14,41 +14,33 @@ import ( ) 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 - } - if ident.Type.Object == nil { - return nil, fmt.Errorf("no type definition for %s", ident.Name) + 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) } - 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 47f6b0ad148..e6488284b81 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -666,7 +666,9 @@ func (r *runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) } } -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 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { 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 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { 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 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { } 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 cefc5395514..31fdf491f3b 100644 --- a/gopls/internal/lsp/source/call_hierarchy.go +++ b/gopls/internal/lsp/source/call_hierarchy.go @@ -15,44 +15,48 @@ import ( "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 { + return nil, err + } + pos, err := pgf.PositionPos(pp) if err != nil { - if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) { - return nil, 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 @@ outer: } // 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 { + return nil, err + } + pos, err := pgf.PositionPos(pp) 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 { + obj := referencedObject(pkg, pgf, pos) + if obj == nil { return nil, nil } - node := identifier.Declaration.node - if node == nil { + + if _, ok := obj.Type().Underlying().(*types.Signature); !ok { return nil, nil } - callExprs, err := collectCallExpressions(identifier.Declaration.nodeFile, node) + + // 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 } - return toProtocolOutgoingCalls(ctx, snapshot, fh, callExprs) -} + // 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 + } -// collectCallExpressions collects call expression ranges inside a function. -func collectCallExpressions(pgf *ParsedGoFile, node ast.Node) ([]protocol.Range, error) { - type callPos struct { - start, end token.Pos + declPos, err := safetoken.Pos(declPGF.Tok, offset) + if err != nil { + return nil, err } - callPositions := []callPos{} - ast.Inspect(node, func(n ast.Node) bool { + 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 + } + 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 @@ func collectCallExpressions(pgf *ParsedGoFile, node ast.Node) ([]protocol.Range, // 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 - } - 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}, + rng, err := declPGF.PosRange(callRange.start, callRange.end) + if err != nil { + return nil, err } + 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 00000000000..d9dd446b451 --- /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 0ed56e472cd..9497c657377 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -30,7 +30,7 @@ type IdentifierInfo struct { Type struct { MappedRange protocol.MappedRange - Object types.Object + Object *types.TypeName } Inferred *types.Signature @@ -48,11 +48,6 @@ type IdentifierInfo struct { 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 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa // 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 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa 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 @@ func searchForEnclosing(info *types.Info, path []ast.Node) *types.TypeName { 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 @@ func typeToObject(typ types.Type) types.Object { 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 bb0b5ecb6d3..aaacc59b8e2 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -140,7 +140,7 @@ func DefaultOptions() *Options { LinksInHover: true, }, NavigationOptions: NavigationOptions{ - ImportShortcut: Both, + ImportShortcut: BothShortcuts, SymbolMatcher: SymbolFastFuzzy, SymbolStyle: DynamicSymbols, }, @@ -596,17 +596,17 @@ type InternalOptions struct { 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 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) 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 00000000000..2a54fdfdbcb --- /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 7dab47b0f57..c9a510224e3 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -51,7 +51,12 @@ func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { 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 @@ func objToMappedRange(ctx context.Context, snapshot Snapshot, pkg Package, obj t 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 FileKindForLang(langID string) FileKind { } } -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 cbcc9b4e6f0..5f4e3f567d9 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -697,6 +697,23 @@ const ( 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 cad69c142c4..70a3336e72b 100644 --- a/gopls/internal/regtest/misc/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -155,12 +155,11 @@ func main() {} ` 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 @@ func main() {} ).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")