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>
This commit is contained in:
Rebecca Stambler 2019-04-04 19:33:08 -04:00
parent 923d258130
commit 1058ed41f4
2 changed files with 86 additions and 81 deletions

View File

@ -6,15 +6,42 @@ package lsp
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source" "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 insertTextFormat := protocol.PlainTextTextFormat
if snippetsSupported { if snippetsSupported {
insertTextFormat = protocol.SnippetTextFormat insertTextFormat = protocol.SnippetTextFormat
@ -24,14 +51,11 @@ func toProtocolCompletionItems(candidates []source.CompletionItem, prefix string
}) })
items := []protocol.CompletionItem{} items := []protocol.CompletionItem{}
for i, candidate := range candidates { for i, candidate := range candidates {
// Matching against the label. // Match against the label.
if !strings.HasPrefix(candidate.Label, prefix) { if !strings.HasPrefix(candidate.Label, prefix) {
continue continue
} }
// InsertText is deprecated in favor of TextEdits. insertText := labelToInsertText(candidate.Label, candidate.Kind, insertTextFormat, usePlaceholders)
// 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)
if strings.HasPrefix(insertText, prefix) { if strings.HasPrefix(insertText, prefix) {
insertText = insertText[len(prefix):] insertText = insertText[len(prefix):]
} }
@ -54,12 +78,6 @@ func toProtocolCompletionItems(candidates []source.CompletionItem, prefix string
FilterText: insertText, FilterText: insertText,
Preselect: i == 0, 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) items = append(items, item)
} }
return items return items
@ -90,13 +108,13 @@ func toProtocolCompletionItemKind(kind source.CompletionItemKind) protocol.Compl
} }
} }
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 { switch kind {
case source.ConstantCompletionItem: case source.ConstantCompletionItem:
// The label for constants is of the format "<identifier> = <value>". // The label for constants is of the format "<identifier> = <value>".
// We should not insert the " = <value>" part of the label. // We should not insert the " = <value>" part of the label.
if i := strings.Index(label, " ="); i >= 0 { if i := strings.Index(label, " ="); i >= 0 {
return label[:i], false return label[:i]
} }
case source.FunctionCompletionItem, source.MethodCompletionItem: case source.FunctionCompletionItem, source.MethodCompletionItem:
var trimmed, params string var trimmed, params string
@ -105,16 +123,16 @@ func labelToProtocolSnippets(label string, kind source.CompletionItemKind, inser
params = strings.Trim(label[i:], "()") params = strings.Trim(label[i:], "()")
} }
if params == "" || trimmed == "" { if params == "" || trimmed == "" {
return label, true return label
} }
// Don't add parameters or parens for the plaintext insert format. // Don't add parameters or parens for the plaintext insert format.
if insertTextFormat == protocol.PlainTextTextFormat { if insertTextFormat == protocol.PlainTextTextFormat {
return trimmed, true return trimmed
} }
// If we do have signature help enabled, the user can see parameters as // If we don't want to use placeholders, just add 2 parentheses with
// they type in the function, so we just return empty parentheses. // the cursor in the middle.
if signatureHelpEnabled { if !usePlaceholders {
return trimmed + "($1)", true return trimmed + "($1)"
} }
// If signature help is not enabled, we should give the user parameters // If signature help is not enabled, we should give the user parameters
// that they can tab through. The insert text format follows the // that they can tab through. The insert text format follows the
@ -131,11 +149,12 @@ func labelToProtocolSnippets(label string, kind source.CompletionItemKind, inser
if i != 0 { if i != 0 {
b.WriteString(", ") 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(')') b.WriteByte(')')
return b.String(), false return b.String()
} }
return label, false return label
} }

View File

@ -75,7 +75,9 @@ type Server struct {
initializedMu sync.Mutex initializedMu sync.Mutex
initialized bool // set once the server has received "initialize" request 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 snippetsSupported bool
configurationSupported bool configurationSupported bool
dynamicConfigurationSupported bool dynamicConfigurationSupported bool
@ -102,36 +104,14 @@ func (s *Server) Initialize(ctx context.Context, params *protocol.InitializePara
} }
s.initialized = true // mark server as initialized now 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 // TODO(rstambler): Change this default to protocol.Incremental (or add a
// flag). Disabled for now to simplify debugging. // flag). Disabled for now to simplify debugging.
s.textDocumentSyncKind = protocol.Full s.textDocumentSyncKind = protocol.Full
//We need a "detached" context so it does not get timeout cancelled. s.setClientCapabilities(params.Capabilities)
//TODO(iancottrell): Do we need to copy any values across?
// 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() viewContext := context.Background()
folders := params.WorkspaceFolders folders := params.WorkspaceFolders
if len(folders) == 0 { if len(folders) == 0 {
@ -195,6 +175,30 @@ func (s *Server) Initialize(ctx context.Context, params *protocol.InitializePara
}, nil }, 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 { func (s *Server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {
if s.configurationSupported { if s.configurationSupported {
if s.dynamicConfigurationSupported { if s.dynamicConfigurationSupported {
@ -346,28 +350,7 @@ func (s *Server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocu
} }
func (s *Server) Completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { func (s *Server) Completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) {
uri := span.NewURI(params.TextDocument.URI) return s.completion(ctx, params)
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
} }
func (s *Server) CompletionResolve(context.Context, *protocol.CompletionItem) (*protocol.CompletionItem, error) { func (s *Server) CompletionResolve(context.Context, *protocol.CompletionItem) (*protocol.CompletionItem, error) {
@ -640,16 +623,19 @@ func (s *Server) processConfig(view *cache.View, config interface{}) error {
if !ok { if !ok {
return fmt.Errorf("invalid config gopls type %T", config) return fmt.Errorf("invalid config gopls type %T", config)
} }
env := c["env"] // Get the environment for the go/packages config.
if env == nil { if env := c["env"]; env != nil {
return 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{}) // Check if placeholders are enabled.
if !ok { if usePlaceholders, ok := c["usePlaceholders"].(bool); ok {
return fmt.Errorf("invalid config gopls.env type %T", env) s.usePlaceholders = usePlaceholders
}
for k, v := range menv {
view.Config.Env = applyEnv(view.Config.Env, k, v)
} }
return nil return nil
} }