Skip to content
This repository was archived by the owner on Oct 12, 2022. It is now read-only.

Commit f328489

Browse files
author
Stephen Gutekanst
committed
langserver: add godef-based hover backend
This change adds a godef-based hover backend, similar to the godef-based `textDocument/definition` backend added prior. The motivation for this change is to avoid typechecking the entire program just to serve a single `textDocument/hover` request. After this change, hover and definition are both very quick and use little resources. Larger requests like `workspace/symbol`, or `textDocument/references`, will continue to use more resources as they must perform typechecking. The output style of this hover implementation does vary from our prior typechecking implementation in slight ways, but overall the implementation always produces results that are on par or slightly better than our typechecking implementation. As with the `textDocument/definition` implementation, we should attempt consolidation of this hover implementation with our typechecking variant further in the future. At this point, of course, they are too different to share much code or implementation. Helps #178 Helps microsoft/vscode-go#853
1 parent 5547cf7 commit f328489

File tree

3 files changed

+344
-10
lines changed

3 files changed

+344
-10
lines changed

langserver/definition.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ var UseBinaryPkgCache = false
2323

2424
func (h *LangHandler) handleDefinition(ctx context.Context, conn jsonrpc2.JSONRPC2, req *jsonrpc2.Request, params lsp.TextDocumentPositionParams) ([]lsp.Location, error) {
2525
if UseBinaryPkgCache {
26-
return h.handleDefinitionGodef(ctx, conn, req, params)
26+
_, _, locs, err := h.definitionGodef(ctx, params)
27+
return locs, err
2728
}
2829

2930
res, err := h.handleXDefinition(ctx, conn, req, params)
@@ -37,28 +38,28 @@ func (h *LangHandler) handleDefinition(ctx context.Context, conn jsonrpc2.JSONRP
3738
return locs, nil
3839
}
3940

40-
func (h *LangHandler) handleDefinitionGodef(ctx context.Context, conn jsonrpc2.JSONRPC2, req *jsonrpc2.Request, params lsp.TextDocumentPositionParams) ([]lsp.Location, error) {
41+
func (h *LangHandler) definitionGodef(ctx context.Context, params lsp.TextDocumentPositionParams) (*token.FileSet, *godef.Result, []lsp.Location, error) {
4142
// Read file contents and calculate byte offset.
4243
filename := h.FilePath(params.TextDocument.URI)
4344
contents, err := ioutil.ReadFile(filename)
4445
if err != nil {
45-
return nil, err
46+
return nil, nil, nil, err
4647
}
4748
offset, valid, why := offsetForPosition(contents, params.Position)
4849
if !valid {
49-
return nil, fmt.Errorf("invalid position: %s:%d:%d (%s)", filename, params.Position.Line, params.Position.Character, why)
50+
return nil, nil, nil, fmt.Errorf("invalid position: %s:%d:%d (%s)", filename, params.Position.Line, params.Position.Character, why)
5051
}
5152

5253
// Invoke godef to determine the position of the definition.
5354
fset := token.NewFileSet()
5455
res, err := godef.Godef(fset, offset, filename, contents)
5556
if err != nil {
56-
return nil, err
57+
return nil, nil, nil, err
5758
}
5859
if res.Package != nil {
5960
// TODO: return directory location. This right now at least matches our
6061
// other implementation.
61-
return []lsp.Location{}, nil
62+
return fset, res, []lsp.Location{}, nil
6263
}
6364
loc := goRangeToLSPLocation(fset, res.Start, res.End)
6465

@@ -69,7 +70,8 @@ func (h *LangHandler) handleDefinitionGodef(ctx context.Context, conn jsonrpc2.J
6970
loc.URI = pathToURI(filepath.Join(build.Default.GOROOT, "/src/builtin/builtin.go"))
7071
loc.Range = lsp.Range{}
7172
}
72-
return []lsp.Location{loc}, nil
73+
74+
return fset, res, []lsp.Location{loc}, nil
7375
}
7476

7577
func (h *LangHandler) handleXDefinition(ctx context.Context, conn jsonrpc2.JSONRPC2, req *jsonrpc2.Request, params lsp.TextDocumentPositionParams) ([]symbolLocationInformation, error) {

langserver/hover.go

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ import (
66
"fmt"
77
"go/ast"
88
"go/build"
9+
"go/format"
10+
"go/parser"
911
"go/token"
1012
"go/types"
13+
"path/filepath"
14+
"sort"
1115
"strings"
1216

1317
doc "github.com/slimsag/godocmd"
@@ -16,6 +20,10 @@ import (
1620
)
1721

1822
func (h *LangHandler) handleHover(ctx context.Context, conn jsonrpc2.JSONRPC2, req *jsonrpc2.Request, params lsp.TextDocumentPositionParams) (*lsp.Hover, error) {
23+
if UseBinaryPkgCache {
24+
return h.handleHoverGodef(ctx, conn, req, params)
25+
}
26+
1927
if !isFileURI(params.TextDocument.URI) {
2028
return nil, &jsonrpc2.Error{
2129
Code: jsonrpc2.CodeInvalidParams,
@@ -245,3 +253,272 @@ func prettyPrintTypesString(s string) string {
245253
}
246254
return b.String()
247255
}
256+
257+
func (h *LangHandler) handleHoverGodef(ctx context.Context, conn jsonrpc2.JSONRPC2, req *jsonrpc2.Request, params lsp.TextDocumentPositionParams) (*lsp.Hover, error) {
258+
// First perform the equivalent of a textDocument/definition request in
259+
// order to resolve the definition position.
260+
fset, res, _, err := h.definitionGodef(ctx, params)
261+
if err != nil {
262+
return nil, err
263+
}
264+
265+
// If our target is a package import statement or package selector, then we
266+
// handle that separately now.
267+
if res.Package != nil {
268+
// res.Package.Name is invalid since it was imported with FindOnly, so
269+
// import normally now.
270+
bpkg, err := build.Default.ImportDir(res.Package.Dir, 0)
271+
if err != nil {
272+
return nil, err
273+
}
274+
275+
// Parse the entire dir into its respective AST packages.
276+
pkgs, err := parser.ParseDir(fset, res.Package.Dir, nil, parser.ParseComments)
277+
if err != nil {
278+
return nil, err
279+
}
280+
pkg := pkgs[bpkg.Name]
281+
282+
// Find the package doc comments.
283+
pkgFiles := make([]*ast.File, 0, len(pkg.Files))
284+
for _, f := range pkg.Files {
285+
pkgFiles = append(pkgFiles, f)
286+
}
287+
comments := packageDoc(pkgFiles, bpkg.Name)
288+
289+
return &lsp.Hover{
290+
Contents: maybeAddComments(comments, []lsp.MarkedString{{Language: "go", Value: fmt.Sprintf("package %s (%q)", bpkg.Name, bpkg.ImportPath)}}),
291+
292+
// TODO(slimsag): I think we can add Range here, but not exactly
293+
// sure. res.Start and res.End are only present if it's a package
294+
// selector, not an import statement. Since Range is optional,
295+
// we're omitting it here.
296+
}, nil
297+
}
298+
299+
loc := goRangeToLSPLocation(fset, res.Start, res.End)
300+
301+
if loc.URI == "file://" {
302+
// TODO: builtins do not have valid URIs or locations.
303+
return &lsp.Hover{}, nil
304+
}
305+
306+
filename := uriToFilePath(loc.URI)
307+
308+
// Parse the entire dir into its respective AST packages.
309+
pkgs, err := parser.ParseDir(fset, filepath.Dir(filename), nil, parser.ParseComments)
310+
if err != nil {
311+
return nil, err
312+
}
313+
314+
// Locate the AST package that contains the file we're interested in.
315+
foundImportPath, foundPackage, err := packageForFile(pkgs, filename)
316+
if err != nil {
317+
return nil, err
318+
}
319+
320+
// Create documentation for the package.
321+
docPkg := doc.New(foundPackage, foundImportPath, doc.AllDecls)
322+
323+
// Locate the target in the docs.
324+
target := fset.Position(res.Start)
325+
docObject := findDocTarget(fset, target, docPkg)
326+
if docObject == nil {
327+
return nil, fmt.Errorf("failed to find doc object for %s", target)
328+
}
329+
330+
contents, node := fmtDocObject(fset, docObject, target)
331+
r := rangeForNode(fset, node)
332+
return &lsp.Hover{
333+
Contents: contents,
334+
Range: &r,
335+
}, nil
336+
}
337+
338+
// packageForFile returns the import path and pkg from pkgs that contains the
339+
// named file.
340+
func packageForFile(pkgs map[string]*ast.Package, filename string) (string, *ast.Package, error) {
341+
for path, pkg := range pkgs {
342+
for pkgFile := range pkg.Files {
343+
if pkgFile == filename {
344+
return path, pkg, nil
345+
}
346+
}
347+
}
348+
return "", nil, fmt.Errorf("failed to find %q in packages %q", filename, pkgs)
349+
}
350+
351+
// inRange tells if x is in the range of a-b inclusive.
352+
func inRange(x, a, b token.Position) bool {
353+
if x.Filename != a.Filename || x.Filename != b.Filename {
354+
return false
355+
}
356+
return x.Offset >= a.Offset && x.Offset <= b.Offset
357+
}
358+
359+
// findDocTarget walks an input *doc.Package and locates the *doc.Value,
360+
// *doc.Type, or *doc.Func for the given target position.
361+
func findDocTarget(fset *token.FileSet, target token.Position, in interface{}) interface{} {
362+
switch v := in.(type) {
363+
case *doc.Package:
364+
for _, x := range v.Consts {
365+
if r := findDocTarget(fset, target, x); r != nil {
366+
return r
367+
}
368+
}
369+
for _, x := range v.Types {
370+
if r := findDocTarget(fset, target, x); r != nil {
371+
return r
372+
}
373+
}
374+
for _, x := range v.Vars {
375+
if r := findDocTarget(fset, target, x); r != nil {
376+
return r
377+
}
378+
}
379+
for _, x := range v.Funcs {
380+
if r := findDocTarget(fset, target, x); r != nil {
381+
return r
382+
}
383+
}
384+
return nil
385+
case *doc.Value:
386+
if inRange(target, fset.Position(v.Decl.Pos()), fset.Position(v.Decl.End())) {
387+
return v
388+
}
389+
return nil
390+
case *doc.Type:
391+
if inRange(target, fset.Position(v.Decl.Pos()), fset.Position(v.Decl.End())) {
392+
return v
393+
}
394+
return nil
395+
case *doc.Func:
396+
if inRange(target, fset.Position(v.Decl.Pos()), fset.Position(v.Decl.End())) {
397+
return v
398+
}
399+
return nil
400+
default:
401+
panic("unreachable")
402+
}
403+
}
404+
405+
// fmtDocObject formats one of:
406+
//
407+
// *doc.Value
408+
// *doc.Type
409+
// *doc.Func
410+
//
411+
func fmtDocObject(fset *token.FileSet, x interface{}, target token.Position) ([]lsp.MarkedString, ast.Node) {
412+
switch v := x.(type) {
413+
case *doc.Value: // Vars and Consts
414+
// Sort the specs by distance to find the one nearest to target.
415+
sort.Sort(byDistance{v.Decl.Specs, fset, target})
416+
spec := v.Decl.Specs[0].(*ast.ValueSpec)
417+
418+
// Use the doc directly above the var inside a var() block, or if there
419+
// is none, fall back to the doc directly above the var() block.
420+
doc := spec.Doc.Text()
421+
if doc == "" {
422+
doc = v.Doc
423+
}
424+
425+
// Create a copy of the spec with no doc for formatting separately.
426+
cpy := *spec
427+
cpy.Doc = nil
428+
value := v.Decl.Tok.String() + " " + fmtNode(fset, &cpy)
429+
return maybeAddComments(doc, []lsp.MarkedString{{Language: "go", Value: value}}), spec
430+
431+
case *doc.Type: // Type declarations
432+
spec := v.Decl.Specs[0].(*ast.TypeSpec)
433+
434+
// Handle interfaces methods and struct fields separately now.
435+
switch s := spec.Type.(type) {
436+
case *ast.InterfaceType:
437+
// Find the method that is an exact match for our target position.
438+
for _, field := range s.Methods.List {
439+
if fset.Position(field.Pos()).Offset == target.Offset {
440+
// An exact match.
441+
value := fmt.Sprintf("func (%s).%s%s", spec.Name.Name, field.Names[0].Name, strings.TrimPrefix(fmtNode(fset, field.Type), "func"))
442+
return maybeAddComments(field.Doc.Text(), []lsp.MarkedString{{Language: "go", Value: value}}), field
443+
}
444+
}
445+
446+
case *ast.StructType:
447+
// Find the field that is an exact match for our target position.
448+
for _, field := range s.Fields.List {
449+
if fset.Position(field.Pos()).Offset == target.Offset {
450+
// An exact match.
451+
value := fmt.Sprintf("struct field %s %s", field.Names[0], fmtNode(fset, field.Type))
452+
return maybeAddComments(field.Doc.Text(), []lsp.MarkedString{{Language: "go", Value: value}}), field
453+
}
454+
}
455+
}
456+
457+
// Formatting of all type declarations: structs, interfaces, integers, etc.
458+
name := v.Decl.Tok.String() + " " + spec.Name.Name + " " + typeName(fset, spec.Type)
459+
res := []lsp.MarkedString{{Language: "go", Value: name}}
460+
461+
doc := spec.Doc.Text()
462+
if doc == "" {
463+
doc = v.Doc
464+
}
465+
res = maybeAddComments(doc, res)
466+
467+
if n := typeName(fset, spec.Type); n == "interface" || n == "struct" {
468+
res = append(res, lsp.MarkedString{Language: "go", Value: fmtNode(fset, spec.Type)})
469+
}
470+
return res, spec
471+
472+
case *doc.Func: // Functions
473+
return maybeAddComments(v.Doc, []lsp.MarkedString{{Language: "go", Value: fmtNode(fset, v.Decl)}}), v.Decl
474+
default:
475+
panic("unreachable")
476+
}
477+
}
478+
479+
// typeName returns the name of typ, shortening interface and struct types to
480+
// just "interface" and "struct" rather than their full contents (incl. methods
481+
// and fields).
482+
func typeName(fset *token.FileSet, typ ast.Expr) string {
483+
switch typ.(type) {
484+
case *ast.InterfaceType:
485+
return "interface"
486+
case *ast.StructType:
487+
return "struct"
488+
default:
489+
return fmtNode(fset, typ)
490+
}
491+
}
492+
493+
// fmtNode formats the given node as a string.
494+
func fmtNode(fset *token.FileSet, n ast.Node) string {
495+
var buf bytes.Buffer
496+
err := format.Node(&buf, fset, n)
497+
if err != nil {
498+
panic("unreachable")
499+
}
500+
return buf.String()
501+
}
502+
503+
// byDistance sorts specs by distance to the target position.
504+
type byDistance struct {
505+
specs []ast.Spec
506+
fset *token.FileSet
507+
target token.Position
508+
}
509+
510+
func (b byDistance) Len() int { return len(b.specs) }
511+
func (b byDistance) Swap(i, j int) { b.specs[i], b.specs[j] = b.specs[j], b.specs[i] }
512+
func (b byDistance) Less(ii, jj int) bool {
513+
i := b.fset.Position(b.specs[ii].Pos())
514+
j := b.fset.Position(b.specs[jj].Pos())
515+
return abs(b.target.Offset-i.Offset) < abs(b.target.Offset-j.Offset)
516+
}
517+
518+
// abs returns the absolute value of x.
519+
func abs(x int) int {
520+
if x < 0 {
521+
return -x
522+
}
523+
return x
524+
}

0 commit comments

Comments
 (0)