internal/lsp: add InsertText to completions

If signature help is enabled, we should not offer parameter suggestions.
If signature help is not enabled, the user should be able to tab
through parameter completions.

Change-Id: I10990ef4aefb306ddbf51ed14cc1110065eba655
Reviewed-on: https://go-review.googlesource.com/c/150637
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
This commit is contained in:
Rebecca Stambler 2018-11-20 16:05:10 -05:00
parent 9c8bd463e3
commit d5eafb537d
3 changed files with 154 additions and 76 deletions

View File

@ -5,22 +5,30 @@
package lsp
import (
"fmt"
"sort"
"strings"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
)
func toProtocolCompletionItems(items []source.CompletionItem) []protocol.CompletionItem {
func toProtocolCompletionItems(items []source.CompletionItem, snippetsSupported, signatureHelpEnabled bool) []protocol.CompletionItem {
var results []protocol.CompletionItem
sort.Slice(items, func(i, j int) bool {
return items[i].Score > items[j].Score
})
insertTextFormat := protocol.PlainTextFormat
if snippetsSupported {
insertTextFormat = protocol.SnippetTextFormat
}
for _, item := range items {
results = append(results, protocol.CompletionItem{
Label: item.Label,
Detail: item.Detail,
Kind: float64(toProtocolCompletionItemKind(item.Kind)),
Label: item.Label,
InsertText: labelToProtocolSnippets(item.Label, item.Kind, insertTextFormat, signatureHelpEnabled),
Detail: item.Detail,
Kind: float64(toProtocolCompletionItemKind(item.Kind)),
InsertTextFormat: insertTextFormat,
})
}
return results
@ -49,5 +57,47 @@ func toProtocolCompletionItemKind(kind source.CompletionItemKind) protocol.Compl
default:
return protocol.TextCompletion
}
}
func labelToProtocolSnippets(label string, kind source.CompletionItemKind, insertTextFormat protocol.InsertTextFormat, signatureHelpEnabled bool) string {
switch kind {
case source.ConstantCompletionItem:
// The label for constants is of the format "<identifier> = <value>".
// We should now insert the " = <value>" part of the label.
return label[:strings.Index(label, " =")]
case source.FunctionCompletionItem, source.MethodCompletionItem:
trimmed := label[:strings.Index(label, "(")]
params := strings.Trim(label[strings.Index(label, "("):], "()")
if params == "" {
return label
}
// Don't add parameters or parens for the plaintext insert format.
if insertTextFormat == protocol.PlainTextFormat {
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)"
}
// If signature help is not enabled, we should give the user parameters
// that they can tab through. The insert text format follows the
// specification defined by Microsoft for LSP. The "$", "}, and "\"
// characters should be escaped.
r := strings.NewReplacer(
`\`, `\\`,
`}`, `\}`,
`$`, `\$`,
)
trimmed += "("
for i, p := range strings.Split(params, ",") {
if i != 0 {
trimmed += ", "
}
trimmed += fmt.Sprintf("${%v:%v}", i+1, r.Replace(strings.Trim(p, " ")))
}
return trimmed + ")"
}
return label
}

View File

@ -31,6 +31,9 @@ func TestLSP(t *testing.T) {
func testLSP(t *testing.T, exporter packagestest.Exporter) {
const dir = "testdata"
// We hardcode the expected number of test cases to ensure that all tests
// are being executed. If a test is added, this number must be changed.
const expectedCompletionsCount = 43
const expectedDiagnosticsCount = 14
const expectedFormatCount = 3
@ -52,23 +55,16 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, modules)
defer exported.Cleanup()
// collect results for certain tests
expectedDiagnostics := make(diagnostics)
completionItems := make(completionItems)
expectedCompletions := make(completions)
expectedFormat := make(formats)
expectedDefinitions := make(definitions)
s := &server{
view: source.NewView(),
}
// merge the config objects
// Merge the exported.Config with the view.Config.
cfg := *exported.Config
cfg.Fset = s.view.Config.Fset
cfg.Mode = packages.LoadSyntax
s.view.Config = &cfg
// Do a first pass to collect special markers
// Do a first pass to collect special markers for completion.
if err := exported.Expect(map[string]interface{}{
"item": func(name string, r packagestest.Range, _, _ string) {
exported.Mark(name, r)
@ -76,6 +72,13 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
}); err != nil {
t.Fatal(err)
}
expectedDiagnostics := make(diagnostics)
completionItems := make(completionItems)
expectedCompletions := make(completions)
expectedFormat := make(formats)
expectedDefinitions := make(definitions)
// Collect any data that needs to be used by subsequent tests.
if err := exported.Expect(map[string]interface{}{
"diag": expectedDiagnostics.collect,
@ -134,66 +137,6 @@ type completions map[token.Position][]token.Pos
type formats map[string]string
type definitions map[protocol.Location]protocol.Location
func (c completions) test(t *testing.T, exported *packagestest.Exported, s *server, items completionItems) {
for src, itemList := range c {
var want []protocol.CompletionItem
for _, pos := range itemList {
want = append(want, *items[pos])
}
list, err := s.Completion(context.Background(), &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: protocol.DocumentURI(source.ToURI(src.Filename)),
},
Position: protocol.Position{
Line: float64(src.Line - 1),
Character: float64(src.Column - 1),
},
},
})
if err != nil {
t.Fatalf("completion failed for %s:%v:%v: %v", filepath.Base(src.Filename), src.Line, src.Column, err)
}
got := list.Items
if equal := reflect.DeepEqual(want, got); !equal {
t.Errorf(diffC(src, want, got))
}
}
}
func (c completions) collect(src token.Position, expected []token.Pos) {
c[src] = expected
}
func (i completionItems) collect(pos token.Pos, label, detail, kind string) {
var k protocol.CompletionItemKind
switch kind {
case "struct":
k = protocol.StructCompletion
case "func":
k = protocol.FunctionCompletion
case "var":
k = protocol.VariableCompletion
case "type":
k = protocol.TypeParameterCompletion
case "field":
k = protocol.FieldCompletion
case "interface":
k = protocol.InterfaceCompletion
case "const":
k = protocol.ConstantCompletion
case "method":
k = protocol.MethodCompletion
case "package":
k = protocol.ModuleCompletion
}
i[pos] = &protocol.CompletionItem{
Label: label,
Detail: detail,
Kind: float64(k),
}
}
func (d diagnostics) test(t *testing.T, exported *packagestest.Exported, v *source.View) int {
count := 0
for filename, want := range d {
@ -241,6 +184,66 @@ func (d diagnostics) collect(pos token.Position, msg string) {
d[pos.Filename] = append(d[pos.Filename], want)
}
func (c completions) test(t *testing.T, exported *packagestest.Exported, s *server, items completionItems) {
for src, itemList := range c {
var want []protocol.CompletionItem
for _, pos := range itemList {
want = append(want, *items[pos])
}
list, err := s.Completion(context.Background(), &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: protocol.DocumentURI(source.ToURI(src.Filename)),
},
Position: protocol.Position{
Line: float64(src.Line - 1),
Character: float64(src.Column - 1),
},
},
})
if err != nil {
t.Fatalf("completion failed for %s:%v:%v: %v", filepath.Base(src.Filename), src.Line, src.Column, err)
}
got := list.Items
if diff := diffC(src, want, got); diff != "" {
t.Errorf(diff)
}
}
}
func (c completions) collect(src token.Position, expected []token.Pos) {
c[src] = expected
}
func (i completionItems) collect(pos token.Pos, label, detail, kind string) {
var k protocol.CompletionItemKind
switch kind {
case "struct":
k = protocol.StructCompletion
case "func":
k = protocol.FunctionCompletion
case "var":
k = protocol.VariableCompletion
case "type":
k = protocol.TypeParameterCompletion
case "field":
k = protocol.FieldCompletion
case "interface":
k = protocol.InterfaceCompletion
case "const":
k = protocol.ConstantCompletion
case "method":
k = protocol.MethodCompletion
case "package":
k = protocol.ModuleCompletion
}
i[pos] = &protocol.CompletionItem{
Label: label,
Detail: detail,
Kind: float64(k),
}
}
func (f formats) test(t *testing.T, s *server) {
for filename, gofmted := range f {
edits, err := s.Formatting(context.Background(), &protocol.DocumentFormattingParams{
@ -313,6 +316,23 @@ func diffD(filename string, want, got []protocol.Diagnostic) string {
// diffC prints the diff between expected and actual completion test results.
func diffC(pos token.Position, want, got []protocol.CompletionItem) string {
if len(got) != len(want) {
goto Failed
}
for i, w := range want {
g := got[i]
if w.Label != g.Label {
goto Failed
}
if w.Detail != g.Detail {
goto Failed
}
if w.Kind != g.Kind {
goto Failed
}
}
return ""
Failed:
msg := &bytes.Buffer{}
fmt.Fprintf(msg, "completion failed for %s:%v:%v:\nexpected:\n", filepath.Base(pos.Filename), pos.Line, pos.Column)
for _, d := range want {

View File

@ -30,6 +30,9 @@ type server struct {
initializedMu sync.Mutex
initialized bool // set once the server has received "initialize" request
signatureHelpEnabled bool
snippetsSupported bool
view *source.View
}
@ -40,7 +43,12 @@ func (s *server) Initialize(ctx context.Context, params *protocol.InitializePara
return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server already initialized")
}
s.view = source.NewView()
s.initialized = true
s.initialized = true // mark server as initialized now
// Check if the client supports snippets in completion items.
s.snippetsSupported = params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport
s.signatureHelpEnabled = true
return &protocol.InitializeResult{
Capabilities: protocol.ServerCapabilities{
CompletionProvider: protocol.CompletionOptions{
@ -167,7 +175,7 @@ func (s *server) Completion(ctx context.Context, params *protocol.CompletionPara
}
return &protocol.CompletionList{
IsIncomplete: false,
Items: toProtocolCompletionItems(items),
Items: toProtocolCompletionItems(items, s.snippetsSupported, s.signatureHelpEnabled),
}, nil
}