internal/lsp: add a "usePlaceholders" setting to gopls configuration

This change allows us to determine if we should complete with
placeholders in the function signature or not.

We currently complete all functions with parameters with a set of
parentheses with the cursor between them. Now, we support the option to
tab through the parameters.

Change-Id: I01d9d7ffdc76eb2f8e009ab9c8aa46d71c24c358
Reviewed-on: https://go-review.googlesource.com/c/tools/+/170877
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go
index c2c8cda..1ea9118 100644
--- a/internal/lsp/completion.go
+++ b/internal/lsp/completion.go
@@ -6,15 +6,42 @@
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"sort"
 	"strings"
 
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/span"
 )
 
-func toProtocolCompletionItems(candidates []source.CompletionItem, prefix string, pos protocol.Position, snippetsSupported, signatureHelpEnabled bool) []protocol.CompletionItem {
+func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) {
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	f, m, err := newColumnMap(ctx, view, uri)
+	if err != nil {
+		return nil, err
+	}
+	spn, err := m.PointSpan(params.Position)
+	if err != nil {
+		return nil, err
+	}
+	rng, err := spn.Range(m.Converter)
+	if err != nil {
+		return nil, err
+	}
+	items, prefix, err := source.Completion(ctx, f, rng.Start)
+	if err != nil {
+		return nil, err
+	}
+	return &protocol.CompletionList{
+		IsIncomplete: false,
+		Items:        toProtocolCompletionItems(items, prefix, params.Position, s.snippetsSupported, s.usePlaceholders),
+	}, nil
+}
+
+func toProtocolCompletionItems(candidates []source.CompletionItem, prefix string, pos protocol.Position, snippetsSupported, usePlaceholders bool) []protocol.CompletionItem {
 	insertTextFormat := protocol.PlainTextTextFormat
 	if snippetsSupported {
 		insertTextFormat = protocol.SnippetTextFormat
@@ -24,14 +51,11 @@
 	})
 	items := []protocol.CompletionItem{}
 	for i, candidate := range candidates {
-		// Matching against the label.
+		// Match against the label.
 		if !strings.HasPrefix(candidate.Label, prefix) {
 			continue
 		}
-		// InsertText is deprecated in favor of TextEdits.
-		// TODO(rstambler): Remove this logic when we are confident that we no
-		// longer need to support it.
-		insertText, triggerSignatureHelp := labelToProtocolSnippets(candidate.Label, candidate.Kind, insertTextFormat, signatureHelpEnabled)
+		insertText := labelToInsertText(candidate.Label, candidate.Kind, insertTextFormat, usePlaceholders)
 		if strings.HasPrefix(insertText, prefix) {
 			insertText = insertText[len(prefix):]
 		}
@@ -54,12 +78,6 @@
 			FilterText: insertText,
 			Preselect:  i == 0,
 		}
-		// If we are completing a function, we should trigger signature help if possible.
-		if triggerSignatureHelp && signatureHelpEnabled {
-			item.Command = &protocol.Command{
-				Command: "editor.action.triggerParameterHints",
-			}
-		}
 		items = append(items, item)
 	}
 	return items
@@ -90,13 +108,13 @@
 	}
 }
 
-func labelToProtocolSnippets(label string, kind source.CompletionItemKind, insertTextFormat protocol.InsertTextFormat, signatureHelpEnabled bool) (string, bool) {
+func labelToInsertText(label string, kind source.CompletionItemKind, insertTextFormat protocol.InsertTextFormat, usePlaceholders bool) string {
 	switch kind {
 	case source.ConstantCompletionItem:
 		// The label for constants is of the format "<identifier> = <value>".
 		// We should not insert the " = <value>" part of the label.
 		if i := strings.Index(label, " ="); i >= 0 {
-			return label[:i], false
+			return label[:i]
 		}
 	case source.FunctionCompletionItem, source.MethodCompletionItem:
 		var trimmed, params string
@@ -105,16 +123,16 @@
 			params = strings.Trim(label[i:], "()")
 		}
 		if params == "" || trimmed == "" {
-			return label, true
+			return label
 		}
 		// Don't add parameters or parens for the plaintext insert format.
 		if insertTextFormat == protocol.PlainTextTextFormat {
-			return trimmed, true
+			return trimmed
 		}
-		// If we do have signature help enabled, the user can see parameters as
-		// they type in the function, so we just return empty parentheses.
-		if signatureHelpEnabled {
-			return trimmed + "($1)", true
+		// If we don't want to use placeholders, just add 2 parentheses with
+		// the cursor in the middle.
+		if !usePlaceholders {
+			return trimmed + "($1)"
 		}
 		// If signature help is not enabled, we should give the user parameters
 		// that they can tab through. The insert text format follows the
@@ -131,11 +149,12 @@
 			if i != 0 {
 				b.WriteString(", ")
 			}
-			fmt.Fprintf(b, "${%v:%v}", i+1, r.Replace(strings.Trim(p, " ")))
+			p = strings.Split(strings.Trim(p, " "), " ")[0]
+			fmt.Fprintf(b, "${%v:%v}", i+1, r.Replace(p))
 		}
 		b.WriteByte(')')
-		return b.String(), false
+		return b.String()
 
 	}
-	return label, false
+	return label
 }
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 01b252e..851b94a 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -75,7 +75,9 @@
 	initializedMu sync.Mutex
 	initialized   bool // set once the server has received "initialize" request
 
-	signatureHelpEnabled          bool
+	// Configurations.
+	// TODO(rstambler): Separate these into their own struct?
+	usePlaceholders               bool
 	snippetsSupported             bool
 	configurationSupported        bool
 	dynamicConfigurationSupported bool
@@ -102,36 +104,14 @@
 	}
 	s.initialized = true // mark server as initialized now
 
-	// Check if the client supports snippets in completion items.
-	if x, ok := params.Capabilities["textDocument"].(map[string]interface{}); ok {
-		if x, ok := x["completion"].(map[string]interface{}); ok {
-			if x, ok := x["completionItem"].(map[string]interface{}); ok {
-				if x, ok := x["snippetSupport"].(bool); ok {
-					s.snippetsSupported = x
-				}
-			}
-		}
-	}
-	// Check if the client supports configuration messages.
-	if x, ok := params.Capabilities["workspace"].(map[string]interface{}); ok {
-		if x, ok := x["configuration"].(bool); ok {
-			s.configurationSupported = x
-		}
-		if x, ok := x["didChangeConfiguration"].(map[string]interface{}); ok {
-			if x, ok := x["dynamicRegistration"].(bool); ok {
-				s.dynamicConfigurationSupported = x
-			}
-		}
-	}
-
-	s.signatureHelpEnabled = true
-
 	// TODO(rstambler): Change this default to protocol.Incremental (or add a
 	// flag). Disabled for now to simplify debugging.
 	s.textDocumentSyncKind = protocol.Full
 
-	//We need a "detached" context so it does not get timeout cancelled.
-	//TODO(iancottrell): Do we need to copy any values across?
+	s.setClientCapabilities(params.Capabilities)
+
+	// We need a "detached" context so it does not get timeout cancelled.
+	// TODO(iancottrell): Do we need to copy any values across?
 	viewContext := context.Background()
 	folders := params.WorkspaceFolders
 	if len(folders) == 0 {
@@ -195,6 +175,30 @@
 	}, nil
 }
 
+func (s *Server) setClientCapabilities(caps protocol.ClientCapabilities) {
+	// Check if the client supports snippets in completion items.
+	if x, ok := caps["textDocument"].(map[string]interface{}); ok {
+		if x, ok := x["completion"].(map[string]interface{}); ok {
+			if x, ok := x["completionItem"].(map[string]interface{}); ok {
+				if x, ok := x["snippetSupport"].(bool); ok {
+					s.snippetsSupported = x
+				}
+			}
+		}
+	}
+	// Check if the client supports configuration messages.
+	if x, ok := caps["workspace"].(map[string]interface{}); ok {
+		if x, ok := x["configuration"].(bool); ok {
+			s.configurationSupported = x
+		}
+		if x, ok := x["didChangeConfiguration"].(map[string]interface{}); ok {
+			if x, ok := x["dynamicRegistration"].(bool); ok {
+				s.dynamicConfigurationSupported = x
+			}
+		}
+	}
+}
+
 func (s *Server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {
 	if s.configurationSupported {
 		if s.dynamicConfigurationSupported {
@@ -346,28 +350,7 @@
 }
 
 func (s *Server) Completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) {
-	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
-	f, m, err := newColumnMap(ctx, view, uri)
-	if err != nil {
-		return nil, err
-	}
-	spn, err := m.PointSpan(params.Position)
-	if err != nil {
-		return nil, err
-	}
-	rng, err := spn.Range(m.Converter)
-	if err != nil {
-		return nil, err
-	}
-	items, prefix, err := source.Completion(ctx, f, rng.Start)
-	if err != nil {
-		return nil, err
-	}
-	return &protocol.CompletionList{
-		IsIncomplete: false,
-		Items:        toProtocolCompletionItems(items, prefix, params.Position, s.snippetsSupported, s.signatureHelpEnabled),
-	}, nil
+	return s.completion(ctx, params)
 }
 
 func (s *Server) CompletionResolve(context.Context, *protocol.CompletionItem) (*protocol.CompletionItem, error) {
@@ -640,16 +623,19 @@
 	if !ok {
 		return fmt.Errorf("invalid config gopls type %T", config)
 	}
-	env := c["env"]
-	if env == nil {
-		return nil
+	// Get the environment for the go/packages config.
+	if env := c["env"]; env != nil {
+		menv, ok := env.(map[string]interface{})
+		if !ok {
+			return fmt.Errorf("invalid config gopls.env type %T", env)
+		}
+		for k, v := range menv {
+			view.Config.Env = applyEnv(view.Config.Env, k, v)
+		}
 	}
-	menv, ok := env.(map[string]interface{})
-	if !ok {
-		return fmt.Errorf("invalid config gopls.env type %T", env)
-	}
-	for k, v := range menv {
-		view.Config.Env = applyEnv(view.Config.Env, k, v)
+	// Check if placeholders are enabled.
+	if usePlaceholders, ok := c["usePlaceholders"].(bool); ok {
+		s.usePlaceholders = usePlaceholders
 	}
 	return nil
 }