internal/lsp: implement signature help

Add SignatureHelp functionality to source package. Tests will be added
in a subsequent change.

Change-Id: Ia43280946d96a984c5741273a00c12255d637b2a
Reviewed-on: https://go-review.googlesource.com/c/149177
Reviewed-by: Ian Cottrell <iancottrell@google.com>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 4c4270f..35d9191 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -43,14 +43,15 @@
 	s.initialized = true
 	return &protocol.InitializeResult{
 		Capabilities: protocol.ServerCapabilities{
+			CompletionProvider:              protocol.CompletionOptions{},
+			DefinitionProvider:              true,
+			DocumentFormattingProvider:      true,
+			DocumentRangeFormattingProvider: true,
+			SignatureHelpProvider:           protocol.SignatureHelpOptions{},
 			TextDocumentSync: protocol.TextDocumentSyncOptions{
 				Change:    float64(protocol.Full), // full contents of file sent on each update
 				OpenClose: true,
 			},
-			DocumentFormattingProvider:      true,
-			DocumentRangeFormattingProvider: true,
-			CompletionProvider:              protocol.CompletionOptions{},
-			DefinitionProvider:              true,
 		},
 	}, nil
 }
@@ -174,8 +175,18 @@
 	return nil, notImplemented("Hover")
 }
 
-func (s *server) SignatureHelp(context.Context, *protocol.TextDocumentPositionParams) (*protocol.SignatureHelp, error) {
-	return nil, notImplemented("SignatureHelp")
+func (s *server) SignatureHelp(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.SignatureHelp, 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)
+	info, err := source.SignatureHelp(ctx, f, pos)
+	if err != nil {
+		return nil, err
+	}
+	return toProtocolSignatureHelp(info), nil
 }
 
 func (s *server) Definition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
diff --git a/internal/lsp/signature_help.go b/internal/lsp/signature_help.go
new file mode 100644
index 0000000..0e54a50
--- /dev/null
+++ b/internal/lsp/signature_help.go
@@ -0,0 +1,33 @@
+// 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 lsp
+
+import (
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
+)
+
+func toProtocolSignatureHelp(info *source.SignatureInformation) *protocol.SignatureHelp {
+	return &protocol.SignatureHelp{
+		ActiveParameter: float64(info.ActiveParameter),
+		ActiveSignature: 0, // there is only ever one possible signature
+		Signatures: []protocol.SignatureInformation{
+			{
+				Label:      info.Label,
+				Parameters: toProtocolParameterInformation(info.Parameters),
+			},
+		},
+	}
+}
+
+func toProtocolParameterInformation(info []source.ParameterInformation) []protocol.ParameterInformation {
+	var result []protocol.ParameterInformation
+	for _, p := range info {
+		result = append(result, protocol.ParameterInformation{
+			Label: p.Label,
+		})
+	}
+	return result
+}
diff --git a/internal/lsp/source/signature_help.go b/internal/lsp/source/signature_help.go
new file mode 100644
index 0000000..ecccdd9
--- /dev/null
+++ b/internal/lsp/source/signature_help.go
@@ -0,0 +1,118 @@
+// 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"
+	"go/types"
+
+	"golang.org/x/tools/go/ast/astutil"
+)
+
+type SignatureInformation struct {
+	Label           string
+	Parameters      []ParameterInformation
+	ActiveParameter int
+}
+
+type ParameterInformation struct {
+	Label string
+}
+
+func SignatureHelp(ctx context.Context, f *File, pos token.Pos) (*SignatureInformation, error) {
+	fAST, err := f.GetAST()
+	if err != nil {
+		return nil, err
+	}
+	pkg, err := f.GetPackage()
+	if err != nil {
+		return nil, err
+	}
+
+	// Find a call expression surrounding the query position.
+	var callExpr *ast.CallExpr
+	path, _ := astutil.PathEnclosingInterval(fAST, pos, pos)
+	if path == nil {
+		return nil, fmt.Errorf("cannot find node enclosing position")
+	}
+	for _, node := range path {
+		if c, ok := node.(*ast.CallExpr); ok {
+			callExpr = c
+			break
+		}
+	}
+	if callExpr == nil || callExpr.Fun == nil {
+		return nil, fmt.Errorf("cannot find an enclosing function")
+	}
+
+	// Get the type information for the function corresponding to the call expression.
+	var obj types.Object
+	switch t := callExpr.Fun.(type) {
+	case *ast.Ident:
+		obj = pkg.TypesInfo.ObjectOf(t)
+	case *ast.SelectorExpr:
+		obj = pkg.TypesInfo.ObjectOf(t.Sel)
+	default:
+		return nil, fmt.Errorf("the enclosing function is malformed")
+	}
+	if obj == nil {
+		return nil, fmt.Errorf("cannot resolve %s", callExpr.Fun)
+	}
+	// Find the signature corresponding to the object.
+	var sig *types.Signature
+	switch obj.(type) {
+	case *types.Var:
+		if underlying, ok := obj.Type().Underlying().(*types.Signature); ok {
+			sig = underlying
+		}
+	case *types.Func:
+		sig = obj.Type().(*types.Signature)
+	}
+	if sig == nil {
+		return nil, fmt.Errorf("no function signatures found for %s", obj.Name())
+	}
+	pkgStringer := qualifier(fAST, pkg.Types, pkg.TypesInfo)
+	var paramInfo []ParameterInformation
+	for i := 0; i < sig.Params().Len(); i++ {
+		param := sig.Params().At(i)
+		label := types.TypeString(param.Type(), pkgStringer)
+		if param.Name() != "" {
+			label = fmt.Sprintf("%s %s", param.Name(), label)
+		}
+		paramInfo = append(paramInfo, ParameterInformation{
+			Label: label,
+		})
+	}
+	// Determine the query position relative to the number of parameters in the function.
+	var activeParam int
+	var start, end token.Pos
+	for i, expr := range callExpr.Args {
+		if start == token.NoPos {
+			start = expr.Pos()
+		}
+		end = expr.End()
+		if i < len(callExpr.Args)-1 {
+			end = callExpr.Args[i+1].Pos() - 1 // comma
+		}
+		if start <= pos && pos <= end {
+			break
+		}
+		activeParam++
+		start = expr.Pos() + 1 // to account for commas
+	}
+	// Label for function, qualified by package name.
+	label := obj.Name()
+	if pkg := pkgStringer(obj.Pkg()); pkg != "" {
+		label = pkg + "." + label
+	}
+	return &SignatureInformation{
+		Label:           label + formatParams(sig.Params(), sig.Variadic(), pkgStringer),
+		Parameters:      paramInfo,
+		ActiveParameter: activeParam,
+	}, nil
+}