internal/lsp: fix completion insertion
The insertion range for completion items was not right. The range's end was 1 before the start. Fix by taking into account the length of the prefix when generating the range start and end. Now instead of a "prefix", we track the completion's "surrounding". This is basically the start and end of the abutting identifier along with the cursor position. When we insert the completion text, we overwrite the entire identifier, not just the prefix. This fixes postfix completion like completing "foo.<>Bar" to "foo.BarBaz". Fixes golang/go#32078 Fixes golang/go#32057 Change-Id: I9d065a413ff9a6e20ae662ff93ad0092c2007c1d GitHub-Last-Rev: af5ab4d60566bf0589d9a712c80d75280178cba9 GitHub-Pull-Request: golang/tools#103 Reviewed-on: https://go-review.googlesource.com/c/tools/+/177757 Run-TryBot: Ian Cottrell <iancottrell@google.com> Reviewed-by: Ian Cottrell <iancottrell@google.com>
This commit is contained in:
parent
1da8801a95
commit
f217c98fae
|
@ -30,29 +30,33 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items, prefix, err := source.Completion(ctx, f, rng.Start)
|
items, surrounding, err := source.Completion(ctx, f, rng.Start)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.session.Logger().Infof(ctx, "no completions found for %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
|
s.session.Logger().Infof(ctx, "no completions found for %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
|
||||||
}
|
}
|
||||||
// We might need to adjust the position to account for the prefix.
|
// We might need to adjust the position to account for the prefix.
|
||||||
prefixRng := protocol.Range{
|
insertionRng := protocol.Range{
|
||||||
Start: params.Position,
|
Start: params.Position,
|
||||||
End: params.Position,
|
End: params.Position,
|
||||||
}
|
}
|
||||||
if prefix.Pos().IsValid() {
|
var prefix string
|
||||||
spn, err := span.NewRange(view.FileSet(), prefix.Pos(), 0).Span()
|
if surrounding != nil {
|
||||||
|
prefix = surrounding.Prefix()
|
||||||
|
spn, err := surrounding.Range.Span()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.session.Logger().Infof(ctx, "failed to get span for prefix position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
|
s.session.Logger().Infof(ctx, "failed to get span for surrounding position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
|
||||||
}
|
|
||||||
if prefixPos, err := m.Position(spn.Start()); err == nil {
|
|
||||||
prefixRng.End = prefixPos
|
|
||||||
} else {
|
} else {
|
||||||
s.session.Logger().Infof(ctx, "failed to convert prefix position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
|
rng, err := m.Range(spn)
|
||||||
|
if err != nil {
|
||||||
|
s.session.Logger().Infof(ctx, "failed to convert surrounding position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
|
||||||
|
} else {
|
||||||
|
insertionRng = rng
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &protocol.CompletionList{
|
return &protocol.CompletionList{
|
||||||
IsIncomplete: false,
|
IsIncomplete: false,
|
||||||
Items: toProtocolCompletionItems(items, prefix.Content(), prefixRng, s.insertTextFormat, s.usePlaceholders),
|
Items: toProtocolCompletionItems(items, prefix, insertionRng, s.insertTextFormat, s.usePlaceholders),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
|
|
||||||
"golang.org/x/tools/go/ast/astutil"
|
"golang.org/x/tools/go/ast/astutil"
|
||||||
"golang.org/x/tools/internal/lsp/snippet"
|
"golang.org/x/tools/internal/lsp/snippet"
|
||||||
|
"golang.org/x/tools/internal/span"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CompletionItem struct {
|
type CompletionItem struct {
|
||||||
|
@ -126,8 +127,8 @@ type completer struct {
|
||||||
// items is the list of completion items returned.
|
// items is the list of completion items returned.
|
||||||
items []CompletionItem
|
items []CompletionItem
|
||||||
|
|
||||||
// prefix is the already-typed portion of the completion candidates.
|
// surrounding describes the identifier surrounding the position.
|
||||||
prefix Prefix
|
surrounding *Selection
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -166,13 +167,32 @@ type compLitInfo struct {
|
||||||
maybeInFieldName bool
|
maybeInFieldName bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Prefix struct {
|
// A Selection represents the cursor position and surrounding identifier.
|
||||||
content string
|
type Selection struct {
|
||||||
pos token.Pos
|
Content string
|
||||||
|
Range span.Range
|
||||||
|
Cursor token.Pos
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Prefix) Content() string { return p.content }
|
func (p Selection) Prefix() string {
|
||||||
func (p Prefix) Pos() token.Pos { return p.pos }
|
return p.Content[:p.Cursor-p.Range.Start]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *completer) setSurrounding(ident *ast.Ident) {
|
||||||
|
if c.surrounding != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(ident.Pos() <= c.pos && c.pos <= ident.End()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.surrounding = &Selection{
|
||||||
|
Content: ident.Name,
|
||||||
|
Range: span.NewRange(c.view.FileSet(), ident.Pos(), ident.End()),
|
||||||
|
Cursor: c.pos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// found adds a candidate completion.
|
// found adds a candidate completion.
|
||||||
//
|
//
|
||||||
|
@ -197,31 +217,31 @@ func (c *completer) found(obj types.Object, weight float64) {
|
||||||
// Completion returns a list of possible candidates for completion, given a
|
// Completion returns a list of possible candidates for completion, given a
|
||||||
// a file and a position.
|
// a file and a position.
|
||||||
//
|
//
|
||||||
// The prefix is computed based on the preceding identifier and can be used by
|
// The selection 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 GoFile, pos token.Pos) ([]CompletionItem, Prefix, error) {
|
func Completion(ctx context.Context, f GoFile, pos token.Pos) ([]CompletionItem, *Selection, 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, Prefix{}, fmt.Errorf("package for %s is ill typed", f.URI())
|
return nil, nil, 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, Prefix{}, fmt.Errorf("cannot find node enclosing position")
|
return nil, nil, 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, Prefix{}, nil
|
return nil, nil, 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, Prefix{}, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
clInfo := enclosingCompositeLiteral(path, pos, pkg.GetTypesInfo())
|
clInfo := enclosingCompositeLiteral(path, pos, pkg.GetTypesInfo())
|
||||||
|
@ -239,12 +259,9 @@ func Completion(ctx context.Context, f GoFile, pos token.Pos) ([]CompletionItem,
|
||||||
enclosingCompositeLiteral: clInfo,
|
enclosingCompositeLiteral: clInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the filter prefix.
|
// Set the filter surrounding.
|
||||||
if ident, ok := path[0].(*ast.Ident); ok {
|
if ident, ok := path[0].(*ast.Ident); ok {
|
||||||
c.prefix = Prefix{
|
c.setSurrounding(ident)
|
||||||
content: ident.Name[:pos-ident.Pos()],
|
|
||||||
pos: ident.Pos(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.expectedType = expectedType(c)
|
c.expectedType = expectedType(c)
|
||||||
|
@ -252,9 +269,9 @@ func Completion(ctx context.Context, f GoFile, pos token.Pos) ([]CompletionItem,
|
||||||
// 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, Prefix{}, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return c.items, c.prefix, nil
|
return c.items, c.surrounding, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch n := path[0].(type) {
|
switch n := path[0].(type) {
|
||||||
|
@ -262,9 +279,9 @@ func Completion(ctx context.Context, f GoFile, pos token.Pos) ([]CompletionItem,
|
||||||
// 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, Prefix{}, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return c.items, c.prefix, nil
|
return c.items, c.surrounding, nil
|
||||||
}
|
}
|
||||||
// reject defining identifiers
|
// reject defining identifiers
|
||||||
if obj, ok := pkg.GetTypesInfo().Defs[n]; ok {
|
if obj, ok := pkg.GetTypesInfo().Defs[n]; ok {
|
||||||
|
@ -276,11 +293,11 @@ func Completion(ctx context.Context, f GoFile, pos token.Pos) ([]CompletionItem,
|
||||||
qual := types.RelativeTo(pkg.GetTypes())
|
qual := types.RelativeTo(pkg.GetTypes())
|
||||||
of += ", of " + types.ObjectString(obj, qual)
|
of += ", of " + types.ObjectString(obj, qual)
|
||||||
}
|
}
|
||||||
return nil, Prefix{}, fmt.Errorf("this is a definition%s", of)
|
return nil, nil, fmt.Errorf("this is a definition%s", of)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := c.lexical(); err != nil {
|
if err := c.lexical(); err != nil {
|
||||||
return nil, Prefix{}, err
|
return nil, nil, 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:
|
||||||
|
@ -288,22 +305,24 @@ func Completion(ctx context.Context, f GoFile, pos token.Pos) ([]CompletionItem,
|
||||||
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, Prefix{}, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
case *ast.SelectorExpr:
|
case *ast.SelectorExpr:
|
||||||
|
c.setSurrounding(n.Sel)
|
||||||
|
|
||||||
if err := c.selector(n); err != nil {
|
if err := c.selector(n); err != nil {
|
||||||
return nil, Prefix{}, err
|
return nil, nil, 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, Prefix{}, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.items, c.prefix, nil
|
return c.items, c.surrounding, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *completer) wantStructFieldCompletions() bool {
|
func (c *completer) wantStructFieldCompletions() bool {
|
||||||
|
|
|
@ -58,9 +58,6 @@ func (c *completer) item(obj types.Object, score float64) CompletionItem {
|
||||||
results, writeParens := formatResults(sig.Results(), c.qf)
|
results, writeParens := formatResults(sig.Results(), c.qf)
|
||||||
label, detail = formatFunction(obj.Name(), params, results, writeParens)
|
label, detail = formatFunction(obj.Name(), params, results, writeParens)
|
||||||
plainSnippet, placeholderSnippet = c.functionCallSnippets(obj.Name(), params)
|
plainSnippet, placeholderSnippet = c.functionCallSnippets(obj.Name(), params)
|
||||||
if plainSnippet == nil && placeholderSnippet == nil {
|
|
||||||
insert = ""
|
|
||||||
}
|
|
||||||
kind = FunctionCompletionItem
|
kind = FunctionCompletionItem
|
||||||
if sig.Recv() != nil {
|
if sig.Recv() != nil {
|
||||||
kind = MethodCompletionItem
|
kind = MethodCompletionItem
|
||||||
|
|
|
@ -135,7 +135,7 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests
|
||||||
}
|
}
|
||||||
tok := f.(source.GoFile).GetToken(ctx)
|
tok := f.(source.GoFile).GetToken(ctx)
|
||||||
pos := tok.Pos(src.Start().Offset())
|
pos := tok.Pos(src.Start().Offset())
|
||||||
list, prefix, err := source.Completion(ctx, f.(source.GoFile), pos)
|
list, surrounding, err := source.Completion(ctx, f.(source.GoFile), pos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed for %v: %v", src, err)
|
t.Fatalf("failed for %v: %v", src, err)
|
||||||
}
|
}
|
||||||
|
@ -145,9 +145,13 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests
|
||||||
if !wantBuiltins && isBuiltin(item) {
|
if !wantBuiltins && isBuiltin(item) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
var prefix string
|
||||||
|
if surrounding != nil {
|
||||||
|
prefix = surrounding.Prefix()
|
||||||
|
}
|
||||||
// We let the client do fuzzy matching, so we return all possible candidates.
|
// We let the client do fuzzy matching, so we return all possible candidates.
|
||||||
// To simplify testing, filter results with prefixes that don't match exactly.
|
// To simplify testing, filter results with prefixes that don't match exactly.
|
||||||
if !strings.HasPrefix(item.Label, prefix.Content()) {
|
if !strings.HasPrefix(item.Label, prefix) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
got = append(got, item)
|
got = append(got, item)
|
||||||
|
|
|
@ -15,7 +15,7 @@ func _() {
|
||||||
|
|
||||||
bar //@snippet(" //", snipBar, "bar(${1})", "bar(${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, "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()")
|
||||||
|
@ -32,9 +32,8 @@ func _() {
|
||||||
Foo{Foo{}.B} //@snippet("} ", snipFieldBar, "Bar", "Bar")
|
Foo{Foo{}.B} //@snippet("} ", snipFieldBar, "Bar", "Bar")
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
err.Error() //@snippet("E", Error, "", "")
|
err.Error() //@snippet("E", Error, "Error", "Error")
|
||||||
f.Baz() //@snippet("B", snipMethodBaz, "", "")
|
f.Baz() //@snippet("B", snipMethodBaz, "Baz", "Baz")
|
||||||
|
|
||||||
// TODO(rstambler): Handle this case correctly.
|
f.Baz() //@snippet("(", snipMethodBazBar, "BazBar", "BazBar")
|
||||||
f.Baz() //@fails("(", snipMethodBazBar, "Bar", "Bar")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ import (
|
||||||
// are being executed. If a test is added, this number must be changed.
|
// are being executed. If a test is added, this number must be changed.
|
||||||
const (
|
const (
|
||||||
ExpectedCompletionsCount = 121
|
ExpectedCompletionsCount = 121
|
||||||
ExpectedCompletionSnippetCount = 13
|
ExpectedCompletionSnippetCount = 14
|
||||||
ExpectedDiagnosticsCount = 17
|
ExpectedDiagnosticsCount = 17
|
||||||
ExpectedFormatCount = 5
|
ExpectedFormatCount = 5
|
||||||
ExpectedDefinitionsCount = 35
|
ExpectedDefinitionsCount = 35
|
||||||
|
|
Loading…
Reference in New Issue