Skip to content

Commit 41e4e56

Browse files
findleyrgopherbot
authored andcommitted
gopls/internal/lsp/source/completion: ensuring completion completeness
Ensure that completion processes at least depth=0 elements, by switching to a different model for truncating search. Don't encode the search deadline in the context, as the handling for context cancellation should differ from the handling of being out of budget. For example, we should not fail to format a completion item if we are out of budget. While at it, don't include type checking time in the completion budget, as it is highly variable and depends on the ordering of requests from the client: for example, if the client has already requested code lens, then the type-checked package will already exist and completion will not include type-checking in the budget. No documentation needs to be updated as the current documentation already says "this normally takes milliseconds", which can only be true if it doesn't include type checking. Also add a regression test that asserts we find all struct fields in completion results. Fixes golang/go#53992 Change-Id: I1aeb749cf64052b6a444166638a78b9945964e84 Reviewed-on: https://go-review.googlesource.com/c/tools/+/503016 Auto-Submit: Robert Findley <rfindley@google.com> Run-TryBot: Robert Findley <rfindley@google.com> Reviewed-by: Alan Donovan <adonovan@google.com> TryBot-Result: Gopher Robot <gobot@golang.org>
1 parent ac29460 commit 41e4e56

File tree

5 files changed

+94
-29
lines changed

5 files changed

+94
-29
lines changed

gopls/internal/lsp/cache/check.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,7 @@ func (ph *packageHandle) clone(validated bool) *packageHandle {
748748
}
749749

750750
// getPackageHandles gets package handles for all given ids and their
751-
// dependencies.
751+
// dependencies, recursively.
752752
func (s *snapshot) getPackageHandles(ctx context.Context, ids []PackageID) (map[PackageID]*packageHandle, error) {
753753
s.mu.Lock()
754754
meta := s.meta

gopls/internal/lsp/source/completion/completion.go

+24-22
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,6 @@ type completer struct {
232232
// mapper converts the positions in the file from which the completion originated.
233233
mapper *protocol.Mapper
234234

235-
// startTime is when we started processing this completion request. It does
236-
// not include any time the request spent in the queue.
237-
startTime time.Time
238-
239235
// scopes contains all scopes defined by nodes in our path,
240236
// including nil values for nodes that don't defined a scope. It
241237
// also includes our package scope and the universal scope at the
@@ -445,8 +441,6 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan
445441
ctx, done := event.Start(ctx, "completion.Completion")
446442
defer done()
447443

448-
startTime := time.Now()
449-
450444
pkg, pgf, err := source.NarrowestPackageForFile(ctx, snapshot, fh.URI())
451445
if err != nil || pgf.File.Package == token.NoPos {
452446
// If we can't parse this file or find position for the package
@@ -555,22 +549,30 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan
555549
matcher: prefixMatcher(""),
556550
methodSetCache: make(map[methodSetKey]*types.MethodSet),
557551
mapper: pgf.Mapper,
558-
startTime: startTime,
559552
scopes: scopes,
560553
}
561554

562-
var cancel context.CancelFunc
563-
if c.opts.budget == 0 {
564-
ctx, cancel = context.WithCancel(ctx)
565-
} else {
566-
// timeoutDuration is the completion budget remaining. If less than
567-
// 10ms, set to 10ms
568-
timeoutDuration := time.Until(c.startTime.Add(c.opts.budget))
569-
if timeoutDuration < 10*time.Millisecond {
570-
timeoutDuration = 10 * time.Millisecond
571-
}
572-
ctx, cancel = context.WithTimeout(ctx, timeoutDuration)
555+
ctx, cancel := context.WithCancel(ctx)
556+
557+
// Compute the deadline for this operation. Deadline is relative to the
558+
// search operation, not the entire completion RPC, as the work up until this
559+
// point depends significantly on how long it took to type-check, which in
560+
// turn depends on the timing of the request relative to other operations on
561+
// the snapshot. Including that work in the budget leads to inconsistent
562+
// results (and realistically, if type-checking took 200ms already, the user
563+
// is unlikely to be significantly more bothered by e.g. another 100ms of
564+
// search).
565+
//
566+
// Don't overload the context with this deadline, as we don't want to
567+
// conflate user cancellation (=fail the operation) with our time limit
568+
// (=stop searching and succeed with partial results).
569+
start := time.Now()
570+
var deadline *time.Time
571+
if c.opts.budget > 0 {
572+
d := start.Add(c.opts.budget)
573+
deadline = &d
573574
}
575+
574576
defer cancel()
575577

576578
if surrounding := c.containingIdent(pgf.Src); surrounding != nil {
@@ -585,7 +587,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan
585587
}
586588

587589
// Deep search collected candidates and their members for more candidates.
588-
c.deepSearch(ctx)
590+
c.deepSearch(ctx, start, deadline)
589591

590592
for _, callback := range c.completionCallbacks {
591593
if err := c.snapshot.RunProcessEnvFunc(ctx, callback); err != nil {
@@ -595,7 +597,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan
595597

596598
// Search candidates populated by expensive operations like
597599
// unimportedMembers etc. for more completion items.
598-
c.deepSearch(ctx)
600+
c.deepSearch(ctx, start, deadline)
599601

600602
// Statement candidates offer an entire statement in certain contexts, as
601603
// opposed to a single object. Add statement candidates last because they
@@ -614,7 +616,7 @@ func (c *completer) collectCompletions(ctx context.Context) error {
614616
if !(importSpec.Path.Pos() <= c.pos && c.pos <= importSpec.Path.End()) {
615617
continue
616618
}
617-
return c.populateImportCompletions(ctx, importSpec)
619+
return c.populateImportCompletions(importSpec)
618620
}
619621

620622
// Inside comments, offer completions for the name of the relevant symbol.
@@ -767,7 +769,7 @@ func (c *completer) emptySwitchStmt() bool {
767769
// Completions for "golang.org/" yield its subdirectories
768770
// (i.e. "golang.org/x/"). The user is meant to accept completion suggestions
769771
// until they reach a complete import path.
770-
func (c *completer) populateImportCompletions(ctx context.Context, searchImport *ast.ImportSpec) error {
772+
func (c *completer) populateImportCompletions(searchImport *ast.ImportSpec) error {
771773
if !strings.HasPrefix(searchImport.Path.Value, `"`) {
772774
return nil
773775
}

gopls/internal/lsp/source/completion/deep_completion.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,17 @@ func (s *deepCompletionState) newPath(cand candidate, obj types.Object) []types.
113113
// deepSearch searches a candidate and its subordinate objects for completion
114114
// items if deep completion is enabled and adds the valid candidates to
115115
// completion items.
116-
func (c *completer) deepSearch(ctx context.Context) {
116+
func (c *completer) deepSearch(ctx context.Context, start time.Time, deadline *time.Time) {
117117
defer func() {
118118
// We can return early before completing the search, so be sure to
119119
// clear out our queues to not impact any further invocations.
120120
c.deepState.thisQueue = c.deepState.thisQueue[:0]
121121
c.deepState.nextQueue = c.deepState.nextQueue[:0]
122122
}()
123123

124-
for len(c.deepState.nextQueue) > 0 {
124+
first := true // always fully process the first set of candidates
125+
for len(c.deepState.nextQueue) > 0 && (first || deadline == nil || time.Now().Before(*deadline)) {
126+
first = false
125127
c.deepState.thisQueue, c.deepState.nextQueue = c.deepState.nextQueue, c.deepState.thisQueue[:0]
126128

127129
outer:
@@ -170,7 +172,7 @@ func (c *completer) deepSearch(ctx context.Context) {
170172

171173
c.deepState.candidateCount++
172174
if c.opts.budget > 0 && c.deepState.candidateCount%100 == 0 {
173-
spent := float64(time.Since(c.startTime)) / float64(c.opts.budget)
175+
spent := float64(time.Since(start)) / float64(c.opts.budget)
174176
select {
175177
case <-ctx.Done():
176178
return

gopls/internal/lsp/source/typerefs/refs_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,7 @@ type P struct{}
485485
486486
func (a) x(P)
487487
`},
488-
want: map[string][]string{},
488+
want: map[string][]string{},
489489
allowErrs: true,
490490
},
491491
{

gopls/internal/regtest/completion/completion_test.go

+63-2
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import (
88
"fmt"
99
"strings"
1010
"testing"
11+
"time"
1112

1213
"github.com/google/go-cmp/cmp"
1314
"golang.org/x/tools/gopls/internal/bug"
1415
"golang.org/x/tools/gopls/internal/hooks"
16+
"golang.org/x/tools/gopls/internal/lsp/fake"
17+
"golang.org/x/tools/gopls/internal/lsp/protocol"
1518
. "golang.org/x/tools/gopls/internal/lsp/regtest"
1619
"golang.org/x/tools/internal/testenv"
17-
18-
"golang.org/x/tools/gopls/internal/lsp/protocol"
1920
)
2021

2122
func TestMain(m *testing.M) {
@@ -622,6 +623,66 @@ func main() {
622623
})
623624
}
624625

626+
func TestCompleteAllFields(t *testing.T) {
627+
// This test verifies that completion results always include all struct fields.
628+
// See golang/go#53992.
629+
630+
const src = `
631+
-- go.mod --
632+
module mod.com
633+
634+
go 1.18
635+
636+
-- p/p.go --
637+
package p
638+
639+
import (
640+
"fmt"
641+
642+
. "net/http"
643+
. "runtime"
644+
. "go/types"
645+
. "go/parser"
646+
. "go/ast"
647+
)
648+
649+
type S struct {
650+
a, b, c, d, e, f, g, h, i, j, k, l, m int
651+
n, o, p, q, r, s, t, u, v, w, x, y, z int
652+
}
653+
654+
func _() {
655+
var s S
656+
fmt.Println(s.)
657+
}
658+
`
659+
660+
WithOptions(Settings{
661+
"completionBudget": "1ns", // must be non-zero as 0 => infinity
662+
}).Run(t, src, func(t *testing.T, env *Env) {
663+
wantFields := make(map[string]bool)
664+
for c := 'a'; c <= 'z'; c++ {
665+
wantFields[string(c)] = true
666+
}
667+
668+
env.OpenFile("p/p.go")
669+
// Make an arbitrary edit to ensure we're not hitting the cache.
670+
env.EditBuffer("p/p.go", fake.NewEdit(0, 0, 0, 0, fmt.Sprintf("// current time: %v\n", time.Now())))
671+
loc := env.RegexpSearch("p/p.go", `s\.()`)
672+
completions := env.Completion(loc)
673+
gotFields := make(map[string]bool)
674+
for _, item := range completions.Items {
675+
if item.Kind == protocol.FieldCompletion {
676+
gotFields[item.Label] = true
677+
}
678+
}
679+
680+
if diff := cmp.Diff(wantFields, gotFields); diff != "" {
681+
t.Errorf("Completion(...) returned mismatching fields (-want +got):\n%s", diff)
682+
}
683+
})
684+
}
685+
625686
func TestDefinition(t *testing.T) {
626687
testenv.NeedsGo1Point(t, 17) // in go1.16, The FieldList in func x is not empty
627688
files := `

0 commit comments

Comments
 (0)