internal/lsp: stop trimming prefix from InsertText
After some discussion about how to handle insert and filter text (https://github.com/microsoft/vscode-languageserver-node/issues/488), it seems that it is better practice to overwrite the prefix in completion items, rather than trimming the prefix from the insert text. Change-Id: I7c794b4b1d4518af31e7318a283aa3681a0cf66a Reviewed-on: https://go-review.googlesource.com/c/tools/+/176958 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:
parent
de15caf068
commit
7d589f28aa
|
@ -33,11 +33,23 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara
|
||||||
items, prefix, err := source.Completion(ctx, f, rng.Start)
|
items, prefix, err := source.Completion(ctx, f, rng.Start)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Infof(ctx, "no completions found for %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
|
s.log.Infof(ctx, "no completions found for %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
|
||||||
items = []source.CompletionItem{}
|
}
|
||||||
|
// We might need to adjust the position to account for the prefix.
|
||||||
|
pos := params.Position
|
||||||
|
if prefix.Pos().IsValid() {
|
||||||
|
spn, err := span.NewRange(view.FileSet(), prefix.Pos(), 0).Span()
|
||||||
|
if err != nil {
|
||||||
|
s.log.Infof(ctx, "failed to get span for prefix position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
|
||||||
|
}
|
||||||
|
if prefixPos, err := m.Position(spn.Start()); err == nil {
|
||||||
|
pos = prefixPos
|
||||||
|
} else {
|
||||||
|
s.log.Infof(ctx, "failed to convert prefix position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &protocol.CompletionList{
|
return &protocol.CompletionList{
|
||||||
IsIncomplete: false,
|
IsIncomplete: false,
|
||||||
Items: toProtocolCompletionItems(items, prefix, params.Position, s.insertTextFormat, s.usePlaceholders),
|
Items: toProtocolCompletionItems(items, prefix.Content(), pos, s.insertTextFormat, s.usePlaceholders),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +57,7 @@ func toProtocolCompletionItems(candidates []source.CompletionItem, prefix string
|
||||||
sort.SliceStable(candidates, func(i, j int) bool {
|
sort.SliceStable(candidates, func(i, j int) bool {
|
||||||
return candidates[i].Score > candidates[j].Score
|
return candidates[i].Score > candidates[j].Score
|
||||||
})
|
})
|
||||||
items := []protocol.CompletionItem{}
|
items := make([]protocol.CompletionItem, 0, len(candidates))
|
||||||
for i, candidate := range candidates {
|
for i, candidate := range candidates {
|
||||||
// Match against the label.
|
// Match against the label.
|
||||||
if !strings.HasPrefix(candidate.Label, prefix) {
|
if !strings.HasPrefix(candidate.Label, prefix) {
|
||||||
|
@ -59,16 +71,6 @@ func toProtocolCompletionItems(candidates []source.CompletionItem, prefix string
|
||||||
insertText = candidate.Snippet.String()
|
insertText = candidate.Snippet.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If the user has already typed some part of the completion candidate,
|
|
||||||
// don't insert that portion of the text.
|
|
||||||
if strings.HasPrefix(insertText, prefix) {
|
|
||||||
insertText = insertText[len(prefix):]
|
|
||||||
}
|
|
||||||
// Don't filter on text that might have snippets in it.
|
|
||||||
filterText := candidate.InsertText
|
|
||||||
if strings.HasPrefix(filterText, prefix) {
|
|
||||||
filterText = filterText[len(prefix):]
|
|
||||||
}
|
|
||||||
item := protocol.CompletionItem{
|
item := protocol.CompletionItem{
|
||||||
Label: candidate.Label,
|
Label: candidate.Label,
|
||||||
Detail: candidate.Detail,
|
Detail: candidate.Detail,
|
||||||
|
@ -85,7 +87,7 @@ func toProtocolCompletionItems(candidates []source.CompletionItem, prefix string
|
||||||
// according to their score. This can be removed upon the resolution of
|
// according to their score. This can be removed upon the resolution of
|
||||||
// https://github.com/Microsoft/language-server-protocol/issues/348.
|
// https://github.com/Microsoft/language-server-protocol/issues/348.
|
||||||
SortText: fmt.Sprintf("%05d", i),
|
SortText: fmt.Sprintf("%05d", i),
|
||||||
FilterText: filterText,
|
FilterText: candidate.InsertText,
|
||||||
Preselect: i == 0,
|
Preselect: i == 0,
|
||||||
}
|
}
|
||||||
// Trigger signature help for any function or method completion.
|
// Trigger signature help for any function or method completion.
|
||||||
|
|
|
@ -113,7 +113,7 @@ type completer struct {
|
||||||
items []CompletionItem
|
items []CompletionItem
|
||||||
|
|
||||||
// prefix is the already-typed portion of the completion candidates.
|
// prefix is the already-typed portion of the completion candidates.
|
||||||
prefix string
|
prefix Prefix
|
||||||
|
|
||||||
// expectedType is the type we expect the completion candidate to be.
|
// expectedType is the type we expect the completion candidate to be.
|
||||||
// It may not be set.
|
// It may not be set.
|
||||||
|
@ -152,6 +152,14 @@ type compLitInfo struct {
|
||||||
maybeInFieldName bool
|
maybeInFieldName bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Prefix struct {
|
||||||
|
content string
|
||||||
|
pos token.Pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Prefix) Content() string { return p.content }
|
||||||
|
func (p Prefix) Pos() token.Pos { return p.pos }
|
||||||
|
|
||||||
// found adds a candidate completion.
|
// found adds a candidate completion.
|
||||||
//
|
//
|
||||||
// Only the first candidate of a given name is considered.
|
// Only the first candidate of a given name is considered.
|
||||||
|
@ -178,28 +186,28 @@ func (c *completer) found(obj types.Object, weight float64) {
|
||||||
// The prefix is computed based on the preceding identifier and can be used by
|
// The prefix is computed based on the preceding identifier and can be used by
|
||||||
// the client to score the quality of the completion. For instance, some clients
|
// the client to score the quality of the completion. For instance, some clients
|
||||||
// may tolerate imperfect matches as valid completion results, since users may make typos.
|
// may tolerate imperfect matches as valid completion results, since users may make typos.
|
||||||
func Completion(ctx context.Context, f File, pos token.Pos) ([]CompletionItem, string, error) {
|
func Completion(ctx context.Context, f File, pos token.Pos) ([]CompletionItem, Prefix, error) {
|
||||||
file := f.GetAST(ctx)
|
file := f.GetAST(ctx)
|
||||||
pkg := f.GetPackage(ctx)
|
pkg := f.GetPackage(ctx)
|
||||||
if pkg == nil || pkg.IsIllTyped() {
|
if pkg == nil || pkg.IsIllTyped() {
|
||||||
return nil, "", fmt.Errorf("package for %s is ill typed", f.URI())
|
return nil, Prefix{}, fmt.Errorf("package for %s is ill typed", f.URI())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completion is based on what precedes the cursor.
|
// Completion is based on what precedes the cursor.
|
||||||
// Find the path to the position before pos.
|
// Find the path to the position before pos.
|
||||||
path, _ := astutil.PathEnclosingInterval(file, pos-1, pos-1)
|
path, _ := astutil.PathEnclosingInterval(file, pos-1, pos-1)
|
||||||
if path == nil {
|
if path == nil {
|
||||||
return nil, "", fmt.Errorf("cannot find node enclosing position")
|
return nil, Prefix{}, fmt.Errorf("cannot find node enclosing position")
|
||||||
}
|
}
|
||||||
// Skip completion inside comments.
|
// Skip completion inside comments.
|
||||||
for _, g := range file.Comments {
|
for _, g := range file.Comments {
|
||||||
if g.Pos() <= pos && pos <= g.End() {
|
if g.Pos() <= pos && pos <= g.End() {
|
||||||
return nil, "", nil
|
return nil, Prefix{}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Skip completion inside any kind of literal.
|
// Skip completion inside any kind of literal.
|
||||||
if _, ok := path[0].(*ast.BasicLit); ok {
|
if _, ok := path[0].(*ast.BasicLit); ok {
|
||||||
return nil, "", nil
|
return nil, Prefix{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
clInfo := enclosingCompositeLiteral(path, pos, pkg.GetTypesInfo())
|
clInfo := enclosingCompositeLiteral(path, pos, pkg.GetTypesInfo())
|
||||||
|
@ -217,9 +225,12 @@ func Completion(ctx context.Context, f File, pos token.Pos) ([]CompletionItem, s
|
||||||
enclosingCompositeLiteral: clInfo,
|
enclosingCompositeLiteral: clInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the filter prefix.
|
||||||
if ident, ok := path[0].(*ast.Ident); ok {
|
if ident, ok := path[0].(*ast.Ident); ok {
|
||||||
// Set the filter prefix.
|
c.prefix = Prefix{
|
||||||
c.prefix = ident.Name[:pos-ident.Pos()]
|
content: ident.Name[:pos-ident.Pos()],
|
||||||
|
pos: ident.Pos(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.expectedType = expectedType(c)
|
c.expectedType = expectedType(c)
|
||||||
|
@ -227,7 +238,7 @@ func Completion(ctx context.Context, f File, pos token.Pos) ([]CompletionItem, s
|
||||||
// Struct literals are handled entirely separately.
|
// Struct literals are handled entirely separately.
|
||||||
if c.wantStructFieldCompletions() {
|
if c.wantStructFieldCompletions() {
|
||||||
if err := c.structLiteralFieldName(); err != nil {
|
if err := c.structLiteralFieldName(); err != nil {
|
||||||
return nil, "", err
|
return nil, Prefix{}, err
|
||||||
}
|
}
|
||||||
return c.items, c.prefix, nil
|
return c.items, c.prefix, nil
|
||||||
}
|
}
|
||||||
|
@ -237,7 +248,7 @@ func Completion(ctx context.Context, f File, pos token.Pos) ([]CompletionItem, s
|
||||||
// Is this the Sel part of a selector?
|
// Is this the Sel part of a selector?
|
||||||
if sel, ok := path[1].(*ast.SelectorExpr); ok && sel.Sel == n {
|
if sel, ok := path[1].(*ast.SelectorExpr); ok && sel.Sel == n {
|
||||||
if err := c.selector(sel); err != nil {
|
if err := c.selector(sel); err != nil {
|
||||||
return nil, "", err
|
return nil, Prefix{}, err
|
||||||
}
|
}
|
||||||
return c.items, c.prefix, nil
|
return c.items, c.prefix, nil
|
||||||
}
|
}
|
||||||
|
@ -251,11 +262,11 @@ func Completion(ctx context.Context, f File, pos token.Pos) ([]CompletionItem, s
|
||||||
qual := types.RelativeTo(pkg.GetTypes())
|
qual := types.RelativeTo(pkg.GetTypes())
|
||||||
of += ", of " + types.ObjectString(obj, qual)
|
of += ", of " + types.ObjectString(obj, qual)
|
||||||
}
|
}
|
||||||
return nil, "", fmt.Errorf("this is a definition%s", of)
|
return nil, Prefix{}, fmt.Errorf("this is a definition%s", of)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := c.lexical(); err != nil {
|
if err := c.lexical(); err != nil {
|
||||||
return nil, "", err
|
return nil, Prefix{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// The function name hasn't been typed yet, but the parens are there:
|
// The function name hasn't been typed yet, but the parens are there:
|
||||||
|
@ -263,18 +274,18 @@ func Completion(ctx context.Context, f File, pos token.Pos) ([]CompletionItem, s
|
||||||
case *ast.TypeAssertExpr:
|
case *ast.TypeAssertExpr:
|
||||||
// Create a fake selector expression.
|
// Create a fake selector expression.
|
||||||
if err := c.selector(&ast.SelectorExpr{X: n.X}); err != nil {
|
if err := c.selector(&ast.SelectorExpr{X: n.X}); err != nil {
|
||||||
return nil, "", err
|
return nil, Prefix{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
case *ast.SelectorExpr:
|
case *ast.SelectorExpr:
|
||||||
if err := c.selector(n); err != nil {
|
if err := c.selector(n); err != nil {
|
||||||
return nil, "", err
|
return nil, Prefix{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// fallback to lexical completions
|
// fallback to lexical completions
|
||||||
if err := c.lexical(); err != nil {
|
if err := c.lexical(); err != nil {
|
||||||
return nil, "", err
|
return nil, Prefix{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -145,8 +145,10 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests
|
||||||
if !wantBuiltins && isBuiltin(item) {
|
if !wantBuiltins && isBuiltin(item) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(item.Label, prefix) {
|
// We let the client do fuzzy matching, so we return all possible candidates.
|
||||||
continue //TODO: why is this needed?
|
// To simplify testing, filter results with prefixes that don't match exactly.
|
||||||
|
if !strings.HasPrefix(item.Label, prefix.Content()) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
got = append(got, item)
|
got = append(got, item)
|
||||||
}
|
}
|
||||||
|
@ -174,7 +176,7 @@ func (r *runner) checkCompletionSnippets(ctx context.Context, t *testing.T, data
|
||||||
}
|
}
|
||||||
tok := f.GetToken(ctx)
|
tok := f.GetToken(ctx)
|
||||||
pos := tok.Pos(src.Start().Offset())
|
pos := tok.Pos(src.Start().Offset())
|
||||||
list, prefix, err := source.Completion(ctx, f, pos)
|
list, _, err := source.Completion(ctx, f, pos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed for %v: %v", src, err)
|
t.Fatalf("failed for %v: %v", src, err)
|
||||||
}
|
}
|
||||||
|
@ -194,9 +196,9 @@ func (r *runner) checkCompletionSnippets(ctx context.Context, t *testing.T, data
|
||||||
|
|
||||||
var expected string
|
var expected string
|
||||||
if usePlaceholders {
|
if usePlaceholders {
|
||||||
expected = prefix + want.PlaceholderSnippet
|
expected = want.PlaceholderSnippet
|
||||||
} else {
|
} else {
|
||||||
expected = prefix + want.PlainSnippet
|
expected = want.PlainSnippet
|
||||||
}
|
}
|
||||||
insertText := gotItem.InsertText
|
insertText := gotItem.InsertText
|
||||||
if usePlaceholders && gotItem.PlaceholderSnippet != nil {
|
if usePlaceholders && gotItem.PlaceholderSnippet != nil {
|
||||||
|
|
|
@ -10,21 +10,21 @@ type Foo struct {
|
||||||
func (Foo) Baz() func() {} //@item(snipMethodBaz, "Baz()", "func()", "field")
|
func (Foo) Baz() func() {} //@item(snipMethodBaz, "Baz()", "func()", "field")
|
||||||
|
|
||||||
func _() {
|
func _() {
|
||||||
f //@snippet(" //", snipFoo, "oo(${1})", "oo(${1:i int}, ${2:b bool})")
|
f //@snippet(" //", snipFoo, "foo(${1})", "foo(${1:i int}, ${2:b bool})")
|
||||||
|
|
||||||
bar //@snippet(" //", snipBar, "(${1})", "(${1:fn func()})")
|
bar //@snippet(" //", snipBar, "bar(${1})", "bar(${1:fn func()})")
|
||||||
|
|
||||||
bar(nil) //@snippet("(", snipBar, "", "")
|
bar(nil) //@snippet("(", snipBar, "bar", "bar")
|
||||||
bar(ba) //@snippet(")", snipBar, "r(${1})", "r(${1:fn func()})")
|
bar(ba) //@snippet(")", snipBar, "bar(${1})", "bar(${1:fn func()})")
|
||||||
var f Foo
|
var f Foo
|
||||||
bar(f.Ba) //@snippet(")", snipMethodBaz, "z()", "z()")
|
bar(f.Ba) //@snippet(")", snipMethodBaz, "Baz()", "Baz()")
|
||||||
|
|
||||||
Foo{
|
Foo{
|
||||||
B //@snippet(" //", snipFieldBar, "ar: ${1},", "ar: ${1:int},")
|
B //@snippet(" //", snipFieldBar, "Bar: ${1},", "Bar: ${1:int},")
|
||||||
}
|
}
|
||||||
|
|
||||||
Foo{B} //@snippet("}", snipFieldBar, "ar: ${1}", "ar: ${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, "ar", "ar")
|
Foo{Foo{}.B} //@snippet("} ", snipFieldBar, "Bar", "Bar")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue