Skip to content

Commit 5ae4576

Browse files
muirdmstamblerre
authored andcommitted
internal/lsp: improve completion after accidental keywords
Sometimes the prefix of the thing you want to complete is a keyword. For example: variance := 123 fmt.Println(var<>) In this case the parser produces an *ast.BadExpr which breaks completion. We now repair this BadExpr by replacing it with an *ast.Ident named "var". We also repair empty decls using a similar approach. This fixes cases like: var typeName string type<> // want to complete to "typeName" We also fix accidental keywords in selectors, such as: foo.var<> The parser produces a phantom "_" in place of the keyword, so we swap it back for an *ast.Ident named "var". In general, though, accidental keywords wreak havoc on the AST so we can only do so much. There are still many cases where a keyword prefix breaks completion. Perhaps in the future the parser can be cursor/in-progress-edit aware and turn accidental keywords into identifiers. Fixes golang/go#34332. PS I tweaked nodeContains() to include n.End() to fix a test failure against tip related to a change to go/parser. When a syntax error is present, an *ast.BlockStmt's End() is now set to the block's final statement's End() (earlier than what it used to be). In order for the cursor pos to test "inside" the block in this case I had to relax the End() comparison. Change-Id: Ib45952cf086cc974f1578298df3dd12829344faa Reviewed-on: https://go-review.googlesource.com/c/tools/+/209438 Run-TryBot: Muir Manders <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]>
1 parent 2b6af5f commit 5ae4576

File tree

4 files changed

+163
-4
lines changed

4 files changed

+163
-4
lines changed

internal/lsp/cache/parse.go

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,21 +231,161 @@ func fix(ctx context.Context, n ast.Node, tok *token.File, src []byte) error {
231231
// Don't propagate this error since *ast.BadExpr is very common
232232
// and it is only sometimes due to array types. Errors from here
233233
// are expected and not actionable in general.
234-
fixArrayErr := fixArrayType(n, parent, tok, src)
235-
if fixArrayErr == nil {
234+
if fixArrayType(n, parent, tok, src) == nil {
236235
// Recursively fix in our fixed node.
237236
err = fix(ctx, parent, tok, src)
237+
return false
238238
}
239+
240+
// Fix cases where the parser expects an expression but finds a keyword, e.g.:
241+
//
242+
// someFunc(var<>) // want to complete to "variance"
243+
//
244+
fixAccidentalKeyword(n, parent, tok, src)
245+
246+
return false
247+
case *ast.DeclStmt:
248+
// Fix cases where the completion prefix looks like a decl, e.g.:
249+
//
250+
// func typeName(obj interface{}) string {}
251+
// type<> // want to call "typeName()" but looks like a "type" decl
252+
//
253+
fixAccidentalDecl(n, parent, tok, src)
239254
return false
255+
case *ast.SelectorExpr:
256+
// Fix cases where a keyword prefix results in a phantom "_" selector, e.g.:
257+
//
258+
// foo.var<> // want to complete to "foo.variance"
259+
//
260+
fixPhantomSelector(n, tok, src)
261+
return true
240262
default:
241263
ancestors = append(ancestors, n)
242264
parent = n
243265
return true
244266
}
245267
})
268+
246269
return err
247270
}
248271

272+
// fixAccidentalDecl tries to fix "accidental" declarations. For example:
273+
//
274+
// func typeOf() {}
275+
// type<> // want to call typeOf(), not declare a type
276+
//
277+
// If we find an *ast.DeclStmt with only a single phantom "_" spec, we
278+
// replace the decl statement with an expression statement containing
279+
// only the keyword. This allows completion to work to some degree.
280+
func fixAccidentalDecl(decl *ast.DeclStmt, parent ast.Node, tok *token.File, src []byte) {
281+
genDecl, _ := decl.Decl.(*ast.GenDecl)
282+
if genDecl == nil || len(genDecl.Specs) != 1 {
283+
return
284+
}
285+
286+
switch spec := genDecl.Specs[0].(type) {
287+
case *ast.TypeSpec:
288+
// If the name isn't a phantom "_" identifier inserted by the
289+
// parser then the decl is likely legitimate and we shouldn't mess
290+
// with it.
291+
if !isPhantomUnderscore(spec.Name, tok, src) {
292+
return
293+
}
294+
case *ast.ValueSpec:
295+
if len(spec.Names) != 1 || !isPhantomUnderscore(spec.Names[0], tok, src) {
296+
return
297+
}
298+
}
299+
300+
replaceNode(parent, decl, &ast.ExprStmt{
301+
X: &ast.Ident{
302+
Name: genDecl.Tok.String(),
303+
NamePos: decl.Pos(),
304+
},
305+
})
306+
}
307+
308+
// fixPhantomSelector tries to fix selector expressions with phantom
309+
// "_" selectors. In particular, we check if the selector is a
310+
// keyword, and if so we swap in an *ast.Ident with the keyword text. For example:
311+
//
312+
// foo.var
313+
//
314+
// yields a "_" selector instead of "var" since "var" is a keyword.
315+
func fixPhantomSelector(sel *ast.SelectorExpr, tok *token.File, src []byte) {
316+
if !isPhantomUnderscore(sel.Sel, tok, src) {
317+
return
318+
}
319+
320+
maybeKeyword := readKeyword(sel.Sel.Pos(), tok, src)
321+
if maybeKeyword == "" {
322+
return
323+
}
324+
325+
replaceNode(sel, sel.Sel, &ast.Ident{
326+
Name: maybeKeyword,
327+
NamePos: sel.Sel.Pos(),
328+
})
329+
}
330+
331+
// isPhantomUnderscore reports whether the given ident is a phantom
332+
// underscore. The parser sometimes inserts phantom underscores when
333+
// it encounters otherwise unparseable situations.
334+
func isPhantomUnderscore(id *ast.Ident, tok *token.File, src []byte) bool {
335+
if id == nil || id.Name != "_" {
336+
return false
337+
}
338+
339+
// Phantom underscore means the underscore is not actually in the
340+
// program text.
341+
offset := tok.Offset(id.Pos())
342+
return len(src) <= offset || src[offset] != '_'
343+
}
344+
345+
// fixAccidentalKeyword tries to fix "accidental" keyword expressions. For example:
346+
//
347+
// variance := 123
348+
// doMath(var<>)
349+
//
350+
// If we find an *ast.BadExpr that begins with a keyword, we replace
351+
// the BadExpr with an *ast.Ident containing the text of the keyword.
352+
// This allows completion to work to some degree.
353+
func fixAccidentalKeyword(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte) {
354+
if !bad.Pos().IsValid() {
355+
return
356+
}
357+
358+
maybeKeyword := readKeyword(bad.Pos(), tok, src)
359+
if maybeKeyword == "" {
360+
return
361+
}
362+
363+
replaceNode(parent, bad, &ast.Ident{Name: maybeKeyword, NamePos: bad.Pos()})
364+
}
365+
366+
// readKeyword reads the keyword starting at pos, if any.
367+
func readKeyword(pos token.Pos, tok *token.File, src []byte) string {
368+
var kwBytes []byte
369+
for i := tok.Offset(pos); i < len(src); i++ {
370+
// Use a simplified identifier check since keywords are always lowercase ASCII.
371+
if src[i] < 'a' || src[i] > 'z' {
372+
break
373+
}
374+
kwBytes = append(kwBytes, src[i])
375+
376+
// Stop search at arbitrarily chosen too-long-for-a-keyword length.
377+
if len(kwBytes) > 15 {
378+
return ""
379+
}
380+
}
381+
382+
if kw := string(kwBytes); token.Lookup(kw).IsKeyword() {
383+
return kw
384+
}
385+
386+
return ""
387+
}
388+
249389
// fixArrayType tries to parse an *ast.BadExpr into an *ast.ArrayType.
250390
// go/parser often turns lone array types like "[]int" into BadExprs
251391
// if it isn't expecting a type.

internal/lsp/source/completion.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,7 @@ func importPath(s *ast.ImportSpec) string {
786786
}
787787

788788
func nodeContains(n ast.Node, pos token.Pos) bool {
789-
return n != nil && n.Pos() <= pos && pos < n.End()
789+
return n != nil && n.Pos() <= pos && pos <= n.End()
790790
}
791791

792792
func (c *completer) inConstDecl() bool {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package keywords
2+
3+
// non-matching candidate - shouldn't show up as completion
4+
var apple = "apple"
5+
6+
func _() {
7+
variance := 123 //@item(kwVariance, "variance", "int", "var")
8+
println(var) //@complete(")", kwVariance)
9+
}
10+
11+
func _() {
12+
var s struct { variance int } //@item(kwVarianceField, "variance", "int", "field")
13+
s.var //@complete(" //", kwVarianceField)
14+
}
15+
16+
func _() {
17+
var typeName string //@item(kwTypeName, "typeName", "string", "var")
18+
type //@complete(" //", kwTypeName)
19+
}

internal/lsp/testdata/summary.txt.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
-- summary --
2-
CompletionsCount = 216
2+
CompletionsCount = 219
33
CompletionSnippetCount = 51
44
UnimportedCompletionsCount = 3
55
DeepCompletionsCount = 5

0 commit comments

Comments
 (0)