Skip to content

Commit

Permalink
improve
Browse files Browse the repository at this point in the history
  • Loading branch information
worlpaker committed Jul 17, 2024
1 parent 049414b commit 831114b
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 46 deletions.
66 changes: 30 additions & 36 deletions gopls/internal/golang/completion/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,8 +340,9 @@ type completionContext struct {
// packageCompletion is true if we are completing a package name.
packageCompletion bool

// removeBeforePeriod is true if we are removing content before the period.
removeBeforePeriod bool
// syntaxError is information about when the source code contains
// syntax errors. Only triggered if the completion candidate has period.
syntaxError syntaxErrorContext
}

// A Selection represents the cursor position and surrounding identifier.
Expand Down Expand Up @@ -768,11 +769,9 @@ func (c *completer) containingIdent(src []byte) *ast.Ident {
// is a keyword. This improves completion after an "accidental
// keyword", e.g. completing to "variance" in "someFunc(var<>)".
return fakeIdent
} else if c.isCompletionAllowed() && tkn == token.IDENT {
// Use manually extracted token even when syntax is incomplete
// or contains errors. This provides better developer experience.
// Only for certain conditions: when completion is allowed and
// the token is an IDENT.
} else if tkn == token.IDENT {
// Use manually extracted token when the source contains
// syntax errors. This provides better developer experience.
return fakeIdent
}

Expand All @@ -781,10 +780,13 @@ func (c *completer) containingIdent(src []byte) *ast.Ident {

// scanToken scans pgh's contents for the token containing pos.
func (c *completer) scanToken(contents []byte) (token.Pos, token.Token, string) {
tok := c.pkg.FileSet().File(c.pos)
var (
lastLit string
prdPos token.Pos
s scanner.Scanner
)

var prdPos token.Pos
var s scanner.Scanner
tok := c.pkg.FileSet().File(c.pos)
s.Init(tok, contents, nil, 0)
for {
tknPos, tkn, lit := s.Scan()
Expand All @@ -794,11 +796,17 @@ func (c *completer) scanToken(contents []byte) (token.Pos, token.Token, string)

if tkn == token.PERIOD {
prdPos = tknPos
// Save the last lit declared just before the period.
c.completionContext.syntaxError.lit = lastLit
}
// Set hasPeriod to true if cursor is:
// - Right after the period (e.g., "foo.<>").
// - One or more characters after the period (e.g., "foo.b<>", "foo.bar<>").
c.completionContext.syntaxError.hasPeriod = tknPos == prdPos || tknPos == prdPos+1

if len(lit) > 0 {
lastLit = lit
}
// Set removeBeforePeriod to true if cursor is:
// - Right after the period (e.g., "foo.")
// - One or more characters after the period (e.g., "foo.b", "foo.bar")
c.completionContext.removeBeforePeriod = tknPos == prdPos || tknPos == prdPos+1

if len(lit) > 0 && tknPos <= c.pos && c.pos <= tknPos+token.Pos(len(lit)) {
return tknPos, tkn, lit
Expand Down Expand Up @@ -1613,6 +1621,14 @@ func (c *completer) lexical(ctx context.Context) error {
continue // Name was declared in some enclosing scope, or not at all.
}

// Provide better completion suggestions when the source code contains syntax
// errors and hasPeriod is true. This helps to offer relevant completions despite
// the presence of syntax errors in the code.
if c.completionContext.syntaxError.hasPeriod {
c.syntaxErrorCompletion(obj)
continue
}

// If obj's type is invalid, find the AST node that defines the lexical block
// containing the declaration of obj. Don't resolve types for packages.
if !isPkgName(obj) && !typeIsValid(obj.Type()) {
Expand Down Expand Up @@ -3371,25 +3387,3 @@ func is[T any](x any) bool {
_, ok := x.(T)
return ok
}

// isCompletionAllowed checks whether completion is allowed even
// if the current source is incomplete or contains syntax errors.
func (c *completer) isCompletionAllowed() bool {
// Examples
//
// BlockStmt:
// minimum, maximum := 0, 0
// minimum, max
// BadExpr:
// math.Sqrt(,0)
// math.Ab
// CallExpr:
// value := 0
// fmt.Println("value:" val)
switch c.path[0].(type) {
case *ast.BlockStmt, *ast.BadExpr, *ast.CallExpr:
return true
default:
return false
}
}
9 changes: 0 additions & 9 deletions gopls/internal/golang/completion/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,6 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e
}
}

// If source contains syntax errors and removeBeforePeriod is true,
// update the label and insert text with the content after the period.
if c.isCompletionAllowed() && c.completionContext.removeBeforePeriod {
_, afterPeriod, found := strings.Cut(insert, ".")
if found {
label = afterPeriod
insert = afterPeriod
}
}
snip.WriteText(insert)

switch obj := obj.(type) {
Expand Down
59 changes: 59 additions & 0 deletions gopls/internal/golang/completion/syntax_error_completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package completion

import (
"go/types"
)

// syntaxErrorContext represents the context of the scenario when
// the source code contains syntax errors during code completion.
type syntaxErrorContext struct {
// hasPeriod is true if we are handling scenarios where the source
// contains syntax errors and the candidate includes the period.
hasPeriod bool
// lit is the literal value of the token that appeared before the period.
lit string
}

// syntaxErrorCompletion provides better code completion when the source contains
// syntax errors and the candidate has periods. Only triggered if hasPeriod is true.
func (c *completer) syntaxErrorCompletion(obj types.Object) {
// Check if the object is equal to the literal before the period.
// If not, check for nested types (e.g., "foo.bar.baz<>").
if obj.Name() != c.completionContext.syntaxError.lit {
c.nestedSynaxErrorCompletion(obj.Type())
return
}

switch obj := obj.(type) {
case *types.PkgName:
c.packageMembers(obj.Imported(), stdScore, nil, c.deepState.enqueue)
default:
c.methodsAndFields(obj.Type(), isVar(obj), nil, c.deepState.enqueue)
}
}

// nestedSynaxErrorCompletion attempts to resolve code completion within nested types
// when the source contains syntax errors. It visits the types to find a match for the literal.
func (c *completer) nestedSynaxErrorCompletion(T types.Type) {
var visit func(T types.Type)
visit = func(T types.Type) {
switch t := T.Underlying().(type) {
case *types.Struct:
for i := 0; i < t.NumFields(); i++ {
field := t.Field(i)
if field.Name() == c.completionContext.syntaxError.lit {
c.methodsAndFields(field.Type(), isVar(field), nil, c.deepState.enqueue)
return
}
if t, ok := field.Type().Underlying().(*types.Struct); ok {
visit(t)
}
}
}
}
visit(T)
}
25 changes: 24 additions & 1 deletion gopls/internal/test/integration/completion/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,23 @@ func test9() {
p := person{}
fmt.Println("name:" p.na)
}
-- test10.go --
package main
type Foo struct {
bar Bar
name string
}
type Bar struct {
baz string
}
func test10() {
f := Foo{}
f.name, f.bar.b
}
`
tests := []struct {
name string
Expand Down Expand Up @@ -1325,7 +1342,13 @@ func test9() {
name: "test 9 struct field completion after missing comma with period",
file: "test9.go",
re: "p.na()",
want: "package main\n\ntype person struct {\n\tname string\n\tage int\n}\n\nfunc test9() {\n\tp := person{}\n\tfmt.Println(\"name:\" p.name)\n}\n",
want: "package main\n\ntype person struct {\n\tname string\n\tage int\n}\n\nfunc test9() {\n\tp := person{}\n\tfmt.Println(\"name:\" p.name)\n}\n\n",
},
{
name: "test 10 complex struct field completion after comma with period",
file: "test10.go",
re: "f.bar.b()",
want: "package main\n\ntype Foo struct {\n\tbar Bar\n\tname string\n}\n\ntype Bar struct {\n\tbaz string\n}\n\nfunc test10() {\n\tf := Foo{}\n\tf.name, f.bar.baz\n}\n",
},
}

Expand Down

0 comments on commit 831114b

Please sign in to comment.