internal/lsp: simplify snippet config/generation

I moved the "usePlaceholders" config field on to CompletionOptions.
This way the completion code generates a single snippet with a little
conditional logic based on the "WantPlaceholders" option instead of
juggling the generation of two almost identical "plain" and
"placeholder" snippets at the same time. It also reduces the work done
generating completion candidates a little.

I also made a minor tweak to the snippet builder where empty
placeholders are now always represented as e.g "${1:}" instead of
"${1}" or "${1:}", depending on if you passed a callback to
WritePlaceholder() or not.

Change-Id: Ib84cc0cd729a11b9e13ad3ac4b6fd2d82460acd5
Reviewed-on: https://go-review.googlesource.com/c/tools/+/193697
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
This commit is contained in:
Muir Manders 2019-09-04 10:23:14 -07:00 committed by Rebecca Stambler
parent 5797d2e298
commit dd2b5c81c5
11 changed files with 90 additions and 96 deletions

View File

@ -70,8 +70,9 @@ func (s *Server) toProtocolCompletionItems(candidates []source.CompletionItem, r
} }
insertText := candidate.InsertText insertText := candidate.InsertText
if options.InsertTextFormat == protocol.SnippetTextFormat { if options.InsertTextFormat == protocol.SnippetTextFormat {
insertText = candidate.Snippet(options.UsePlaceholders) insertText = candidate.Snippet()
} }
item := protocol.CompletionItem{ item := protocol.CompletionItem{
Label: candidate.Label, Label: candidate.Label,
Detail: candidate.Detail, Detail: candidate.Detail,

View File

@ -322,7 +322,7 @@ func (s *Server) processConfig(ctx context.Context, view source.View, options *s
setBool(&options.Completion.Unimported, c, "wantUnimportedCompletions") setBool(&options.Completion.Unimported, c, "wantUnimportedCompletions")
// If the user wants placeholders for autocompletion results. // If the user wants placeholders for autocompletion results.
setBool(&options.UsePlaceholders, c, "usePlaceholders") setBool(&options.Completion.Placeholders, c, "usePlaceholders")
return nil return nil
} }

View File

@ -153,12 +153,11 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests
modified.InsertTextFormat = protocol.SnippetTextFormat modified.InsertTextFormat = protocol.SnippetTextFormat
for _, usePlaceholders := range []bool{true, false} { for _, usePlaceholders := range []bool{true, false} {
modified.UsePlaceholders = usePlaceholders
for src, want := range snippets { for src, want := range snippets {
modified.Completion.Deep = strings.Contains(string(src.URI()), "deepcomplete") modified.Completion.Deep = strings.Contains(string(src.URI()), "deepcomplete")
modified.Completion.FuzzyMatching = strings.Contains(string(src.URI()), "fuzzymatch") modified.Completion.FuzzyMatching = strings.Contains(string(src.URI()), "fuzzymatch")
modified.Completion.Unimported = strings.Contains(string(src.URI()), "unimported") modified.Completion.Unimported = strings.Contains(string(src.URI()), "unimported")
modified.Completion.Placeholders = usePlaceholders
r.server.session.SetOptions(modified) r.server.session.SetOptions(modified)
list := r.runCompletion(t, src) list := r.runCompletion(t, src)

View File

@ -43,9 +43,8 @@ func (b *Builder) WriteText(s string) {
// The callback style allows for creating nested placeholders. To write an // The callback style allows for creating nested placeholders. To write an
// empty tab stop, provide a nil callback. // empty tab stop, provide a nil callback.
func (b *Builder) WritePlaceholder(fn func(*Builder)) { func (b *Builder) WritePlaceholder(fn func(*Builder)) {
fmt.Fprintf(&b.sb, "${%d", b.nextTabStop()) fmt.Fprintf(&b.sb, "${%d:", b.nextTabStop())
if fn != nil { if fn != nil {
b.sb.WriteByte(':')
fn(b) fn(b)
} }
b.sb.WriteByte('}') b.sb.WriteByte('}')

View File

@ -10,6 +10,8 @@ import (
func TestSnippetBuilder(t *testing.T) { func TestSnippetBuilder(t *testing.T) {
expect := func(expected string, fn func(*Builder)) { expect := func(expected string, fn func(*Builder)) {
t.Helper()
var b Builder var b Builder
fn(&b) fn(&b)
if got := b.String(); got != expected { if got := b.String(); got != expected {
@ -23,7 +25,7 @@ func TestSnippetBuilder(t *testing.T) {
b.WriteText(`hi { } $ | " , / \`) b.WriteText(`hi { } $ | " , / \`)
}) })
expect("${1}", func(b *Builder) { expect("${1:}", func(b *Builder) {
b.WritePlaceholder(nil) b.WritePlaceholder(nil)
}) })

View File

@ -55,21 +55,9 @@ type CompletionItem struct {
// A higher score indicates that this completion item is more relevant. // A higher score indicates that this completion item is more relevant.
Score float64 Score float64
// Snippet is the LSP snippet for the completion item, without placeholders. // snippet is the LSP snippet for the completion item. The LSP
// The LSP specification contains details about LSP snippets. // specification contains details about LSP snippets. For example, a
// For example, a snippet for a function with the following signature: // snippet for a function with the following signature:
//
// func foo(a, b, c int)
//
// would be:
//
// foo(${1:})
//
plainSnippet *snippet.Builder
// PlaceholderSnippet is the LSP snippet for the completion ite, containing
// placeholders. The LSP specification contains details about LSP snippets.
// For example, a placeholder snippet for a function with the following signature:
// //
// func foo(a, b, c int) // func foo(a, b, c int)
// //
@ -77,22 +65,22 @@ type CompletionItem struct {
// //
// foo(${1:a int}, ${2: b int}, ${3: c int}) // foo(${1:a int}, ${2: b int}, ${3: c int})
// //
placeholderSnippet *snippet.Builder // If Placeholders is false in the CompletionOptions, the above
// snippet would instead be:
//
// foo(${1:})
snippet *snippet.Builder
// Documentation is the documentation for the completion item. // Documentation is the documentation for the completion item.
Documentation string Documentation string
} }
// Snippet is a convenience function that determines the snippet that should be // Snippet is a convenience returns the snippet if available, otherwise
// the InsertText.
// used for an item, depending on if the callee wants placeholders or not. // used for an item, depending on if the callee wants placeholders or not.
func (i *CompletionItem) Snippet(usePlaceholders bool) string { func (i *CompletionItem) Snippet() string {
if usePlaceholders { if i.snippet != nil {
if i.placeholderSnippet != nil { return i.snippet.String()
return i.placeholderSnippet.String()
}
}
if i.plainSnippet != nil {
return i.plainSnippet.String()
} }
return i.InsertText return i.InsertText
} }

View File

@ -30,20 +30,19 @@ func (c *completer) item(cand candidate) (CompletionItem, error) {
} }
var ( var (
label = cand.name label = cand.name
detail = types.TypeString(obj.Type(), c.qf) detail = types.TypeString(obj.Type(), c.qf)
insert = label insert = label
kind CompletionItemKind kind CompletionItemKind
plainSnippet *snippet.Builder snip *snippet.Builder
placeholderSnippet *snippet.Builder protocolEdits []protocol.TextEdit
protocolEdits []protocol.TextEdit
) )
// expandFuncCall mutates the completion label, detail, and snippets // expandFuncCall mutates the completion label, detail, and snippet
// to that of an invocation of sig. // to that of an invocation of sig.
expandFuncCall := func(sig *types.Signature) { expandFuncCall := func(sig *types.Signature) {
params := formatParams(sig.Params(), sig.Variadic(), c.qf) params := formatParams(sig.Params(), sig.Variadic(), c.qf)
plainSnippet, placeholderSnippet = c.functionCallSnippets(label, params) snip = c.functionCallSnippet(label, params)
results, writeParens := formatResults(sig.Results(), c.qf) results, writeParens := formatResults(sig.Results(), c.qf)
detail = "func" + formatFunction(params, results, writeParens) detail = "func" + formatFunction(params, results, writeParens)
} }
@ -59,7 +58,7 @@ func (c *completer) item(cand candidate) (CompletionItem, error) {
} }
if obj.IsField() { if obj.IsField() {
kind = FieldCompletionItem kind = FieldCompletionItem
plainSnippet, placeholderSnippet = c.structFieldSnippets(label, detail) snip = c.structFieldSnippet(label, detail)
} else if c.isParameter(obj) { } else if c.isParameter(obj) {
kind = ParameterCompletionItem kind = ParameterCompletionItem
} else { } else {
@ -110,8 +109,7 @@ func (c *completer) item(cand candidate) (CompletionItem, error) {
Kind: kind, Kind: kind,
Score: cand.score, Score: cand.score,
Depth: len(c.deepState.chain), Depth: len(c.deepState.chain),
plainSnippet: plainSnippet, snippet: snip,
placeholderSnippet: placeholderSnippet,
} }
// If the user doesn't want documentation for completion items. // If the user doesn't want documentation for completion items.
if !c.opts.Documentation { if !c.opts.Documentation {
@ -200,7 +198,7 @@ func (c *completer) formatBuiltin(cand candidate) CompletionItem {
results, writeResultParens := formatFieldList(c.ctx, c.view, decl.Type.Results) results, writeResultParens := formatFieldList(c.ctx, c.view, decl.Type.Results)
item.Label = obj.Name() item.Label = obj.Name()
item.Detail = "func" + formatFunction(params, results, writeResultParens) item.Detail = "func" + formatFunction(params, results, writeResultParens)
item.plainSnippet, item.placeholderSnippet = c.functionCallSnippets(obj.Name(), params) item.snippet = c.functionCallSnippet(obj.Name(), params)
case *types.TypeName: case *types.TypeName:
if types.IsInterface(obj.Type()) { if types.IsInterface(obj.Type()) {
item.Kind = InterfaceCompletionItem item.Kind = InterfaceCompletionItem

View File

@ -5,57 +5,53 @@
package source package source
import ( import (
"fmt"
"go/ast" "go/ast"
"golang.org/x/tools/internal/lsp/snippet" "golang.org/x/tools/internal/lsp/snippet"
) )
// structFieldSnippets calculates the plain and placeholder snippets for struct literal field names. // structFieldSnippets calculates the snippet for struct literal field names.
func (c *completer) structFieldSnippets(label, detail string) (*snippet.Builder, *snippet.Builder) { func (c *completer) structFieldSnippet(label, detail string) *snippet.Builder {
if !c.wantStructFieldCompletions() { if !c.wantStructFieldCompletions() {
return nil, nil return nil
} }
// If we are in a deep completion then we can't be completing a field // If we are in a deep completion then we can't be completing a field
// name (e.g. "Foo{f<>}" completing to "Foo{f.Bar}" should not generate // name (e.g. "Foo{f<>}" completing to "Foo{f.Bar}" should not generate
// a snippet). // a snippet).
if c.inDeepCompletion() { if c.inDeepCompletion() {
return nil, nil return nil
} }
clInfo := c.enclosingCompositeLiteral clInfo := c.enclosingCompositeLiteral
// If we are already in a key-value expression, we don't want a snippet. // If we are already in a key-value expression, we don't want a snippet.
if clInfo.kv != nil { if clInfo.kv != nil {
return nil, nil return nil
} }
plain, placeholder := &snippet.Builder{}, &snippet.Builder{} snip := &snippet.Builder{}
label = fmt.Sprintf("%s: ", label)
// A plain snippet turns "Foo{Ba<>" into "Foo{Bar: <>". // A plain snippet turns "Foo{Ba<>" into "Foo{Bar: <>".
plain.WriteText(label) snip.WriteText(label + ": ")
plain.WritePlaceholder(nil) snip.WritePlaceholder(func(b *snippet.Builder) {
// A placeholder snippet turns "Foo{Ba<>" into "Foo{Bar: <*int*>".
// A placeholder snippet turns "Foo{Ba<>" into "Foo{Bar: <*int*>". if c.opts.Placeholders {
placeholder.WriteText(label) b.WriteText(detail)
placeholder.WritePlaceholder(func(b *snippet.Builder) { }
b.WriteText(detail)
}) })
// If the cursor position is on a different line from the literal's opening brace, // If the cursor position is on a different line from the literal's opening brace,
// we are in a multiline literal. // we are in a multiline literal.
if c.view.Session().Cache().FileSet().Position(c.pos).Line != c.view.Session().Cache().FileSet().Position(clInfo.cl.Lbrace).Line { if c.view.Session().Cache().FileSet().Position(c.pos).Line != c.view.Session().Cache().FileSet().Position(clInfo.cl.Lbrace).Line {
plain.WriteText(",") snip.WriteText(",")
placeholder.WriteText(",")
} }
return plain, placeholder return snip
} }
// functionCallSnippets calculates the plain and placeholder snippets for function calls. // functionCallSnippets calculates the snippet for function calls.
func (c *completer) functionCallSnippets(name string, params []string) (*snippet.Builder, *snippet.Builder) { func (c *completer) functionCallSnippet(name string, params []string) *snippet.Builder {
// If we are the left side (i.e. "Fun") part of a call expression, // If we are the left side (i.e. "Fun") part of a call expression,
// we don't want a snippet since there are already parens present. // we don't want a snippet since there are already parens present.
if len(c.path) > 1 { if len(c.path) > 1 {
@ -65,37 +61,37 @@ func (c *completer) functionCallSnippets(name string, params []string) (*snippet
// inserted when fixing the AST. In this case, we do still need // inserted when fixing the AST. In this case, we do still need
// to insert the calling "()" parens. // to insert the calling "()" parens.
if n.Fun == c.path[0] && n.Lparen != n.Rparen { if n.Fun == c.path[0] && n.Lparen != n.Rparen {
return nil, nil return nil
} }
case *ast.SelectorExpr: case *ast.SelectorExpr:
if len(c.path) > 2 { if len(c.path) > 2 {
if call, ok := c.path[2].(*ast.CallExpr); ok && call.Fun == c.path[1] && call.Lparen != call.Rparen { if call, ok := c.path[2].(*ast.CallExpr); ok && call.Fun == c.path[1] && call.Lparen != call.Rparen {
return nil, nil return nil
} }
} }
} }
} }
plain, placeholder := &snippet.Builder{}, &snippet.Builder{} snip := &snippet.Builder{}
label := fmt.Sprintf("%s(", name) snip.WriteText(name + "(")
// A plain snippet turns "someFun<>" into "someFunc(<>)". if c.opts.Placeholders {
plain.WriteText(label) // A placeholder snippet turns "someFun<>" into "someFunc(<*i int*>, *s string*)".
if len(params) > 0 { for i, p := range params {
plain.WritePlaceholder(nil) if i > 0 {
} snip.WriteText(", ")
plain.WriteText(")") }
snip.WritePlaceholder(func(b *snippet.Builder) {
// A placeholder snippet turns "someFun<>" into "someFunc(<*i int*>, *s string*)". b.WriteText(p)
placeholder.WriteText(label) })
for i, p := range params { }
if i > 0 { } else {
placeholder.WriteText(", ") // A plain snippet turns "someFun<>" into "someFunc(<>)".
if len(params) > 0 {
snip.WritePlaceholder(nil)
} }
placeholder.WritePlaceholder(func(b *snippet.Builder) {
b.WriteText(p)
})
} }
placeholder.WriteText(")")
return plain, placeholder snip.WriteText(")")
return snip
} }

View File

@ -31,7 +31,6 @@ var (
type SessionOptions struct { type SessionOptions struct {
Env []string Env []string
BuildFlags []string BuildFlags []string
UsePlaceholders bool
HoverKind HoverKind HoverKind HoverKind
DisabledAnalyses map[string]struct{} DisabledAnalyses map[string]struct{}
@ -59,6 +58,7 @@ type CompletionOptions struct {
Unimported bool Unimported bool
Documentation bool Documentation bool
FullDocumentation bool FullDocumentation bool
Placeholders bool
} }
type HoverKind int type HoverKind int

View File

@ -167,6 +167,7 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests
Documentation: true, Documentation: true,
Deep: strings.Contains(string(src.URI()), "deepcomplete"), Deep: strings.Contains(string(src.URI()), "deepcomplete"),
FuzzyMatching: strings.Contains(string(src.URI()), "fuzzymatch"), FuzzyMatching: strings.Contains(string(src.URI()), "fuzzymatch"),
Placeholders: usePlaceholders,
}) })
if err != nil { if err != nil {
t.Fatalf("failed for %v: %v", src, err) t.Fatalf("failed for %v: %v", src, err)
@ -186,8 +187,18 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests
if usePlaceholders { if usePlaceholders {
expected = want.PlaceholderSnippet expected = want.PlaceholderSnippet
} }
if actual := got.Snippet(usePlaceholders); expected != actual { if expected == "" {
t.Errorf("%s: expected placeholder snippet %q, got %q", src, expected, actual) if got != nil {
t.Fatalf("%s:%d: expected no matching snippet", src.URI(), src.Start().Line())
}
} else {
if got == nil {
t.Fatalf("%s:%d: couldn't find completion matching %q", src.URI(), src.Start().Line(), wantItem.Label)
}
actual := got.Snippet()
if expected != actual {
t.Errorf("%s: expected placeholder snippet %q, got %q", src, expected, actual)
}
} }
} }
} }

View File

@ -11,23 +11,23 @@ func (Foo) Baz() func() {} //@item(snipMethodBaz, "Baz", "func() func()", "metho
func (Foo) BazBar() func() {} //@item(snipMethodBazBar, "BazBar", "func() func()", "method") func (Foo) BazBar() func() {} //@item(snipMethodBazBar, "BazBar", "func() func()", "method")
func _() { func _() {
f //@snippet(" //", snipFoo, "foo(${1})", "foo(${1:i int}, ${2:b bool})") f //@snippet(" //", snipFoo, "foo(${1:})", "foo(${1:i int}, ${2:b bool})")
bar //@snippet(" //", snipBar, "bar(${1})", "bar(${1:fn func()})") bar //@snippet(" //", snipBar, "bar(${1:})", "bar(${1:fn func()})")
bar(nil) //@snippet("(", snipBar, "bar", "bar") bar(nil) //@snippet("(", snipBar, "bar", "bar")
bar(ba) //@snippet(")", snipBar, "bar(${1})", "bar(${1:fn func()})") bar(ba) //@snippet(")", snipBar, "bar(${1:})", "bar(${1:fn func()})")
var f Foo var f Foo
bar(f.Ba) //@snippet(")", snipMethodBaz, "Baz()", "Baz()") bar(f.Ba) //@snippet(")", snipMethodBaz, "Baz()", "Baz()")
(bar)(nil) //@snippet(")", snipBar, "bar(${1})", "bar(${1:fn func()})") (bar)(nil) //@snippet(")", snipBar, "bar(${1:})", "bar(${1:fn func()})")
(f.Ba)() //@snippet(")", snipMethodBaz, "Baz()", "Baz()") (f.Ba)() //@snippet(")", snipMethodBaz, "Baz()", "Baz()")
Foo{ Foo{
B //@snippet(" //", snipFieldBar, "Bar: ${1},", "Bar: ${1:int},") B //@snippet(" //", snipFieldBar, "Bar: ${1:},", "Bar: ${1:int},")
} }
Foo{B} //@snippet("}", snipFieldBar, "Bar: ${1}", "Bar: ${1:int}") Foo{B} //@snippet("}", snipFieldBar, "Bar: ${1:}", "Bar: ${1:int}")
Foo{} //@snippet("}", snipFieldBar, "Bar: ${1}", "Bar: ${1:int}") Foo{} //@snippet("}", snipFieldBar, "Bar: ${1:}", "Bar: ${1:int}")
Foo{Foo{}.B} //@snippet("} ", snipFieldBar, "Bar", "Bar") Foo{Foo{}.B} //@snippet("} ", snipFieldBar, "Bar", "Bar")