diff --git a/internal/lsp/source/completion/completion.go b/internal/lsp/source/completion/completion.go index 1cbb0a676d..350b71ef3d 100644 --- a/internal/lsp/source/completion/completion.go +++ b/internal/lsp/source/completion/completion.go @@ -158,6 +158,9 @@ type completer struct { // triggerCharacter is the character that triggered this request, if any. triggerCharacter string + // fh is a handle to the file associated with this completion request. + fh source.FileHandle + // filename is the name of the file associated with this completion request. filename string @@ -456,6 +459,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan snapshot: snapshot, qf: source.Qualifier(pgf.File, pkg.GetTypes(), pkg.GetTypesInfo()), triggerCharacter: triggerCharacter, + fh: fh, filename: fh.URI().Filename(), file: pgf.File, path: path, @@ -497,27 +501,42 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan c.inference = expectedCandidate(ctx, c) - defer c.sortItems() + err = c.collectCompletions(ctx) + if err != nil { + return nil, nil, err + } + // Deep search collected candidates and their members for more candidates. + c.deepSearch(ctx) + + // Statement candidates offer an entire statement in certain contexts, as + // opposed to a single object. Add statement candidates last because they + // depend on other candidates having already been collected. + c.addStatementCandidates() + + c.sortItems() + return c.items, c.getSurrounding(), nil +} + +// collectCompletions adds possible completion candidates to either the deep +// search queue or completion items directly for different completion contexts. +func (c *completer) collectCompletions(ctx context.Context) error { // Inside import blocks, return completions for unimported packages. - for _, importSpec := range pgf.File.Imports { - if !(importSpec.Path.Pos() <= rng.Start && rng.Start <= importSpec.Path.End()) { + for _, importSpec := range c.file.Imports { + if !(importSpec.Path.Pos() <= c.pos && c.pos <= importSpec.Path.End()) { continue } - if err := c.populateImportCompletions(ctx, importSpec); err != nil { - return nil, nil, err - } - return c.items, c.getSurrounding(), nil + return c.populateImportCompletions(ctx, importSpec) } // Inside comments, offer completions for the name of the relevant symbol. - for _, comment := range pgf.File.Comments { - if comment.Pos() < rng.Start && rng.Start <= comment.End() { + for _, comment := range c.file.Comments { + if comment.Pos() < c.pos && c.pos <= comment.End() { // Deep completion doesn't work properly in comments since we don't // have a type object to complete further. c.deepState.enabled = false c.populateCommentCompletions(ctx, comment) - return c.items, c.getSurrounding(), nil + return nil } } @@ -528,69 +547,45 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan if c.enclosingCompositeLiteral.inKey { c.deepState.enabled = false } - if err := c.structLiteralFieldName(ctx); err != nil { - return nil, nil, err - } - return c.items, c.getSurrounding(), nil + return c.structLiteralFieldName(ctx) } if lt := c.wantLabelCompletion(); lt != labelNone { c.labels(ctx, lt) - return c.items, c.getSurrounding(), nil + return nil } if c.emptySwitchStmt() { // Empty switch statements only admit "default" and "case" keywords. c.addKeywordItems(map[string]bool{}, highScore, CASE, DEFAULT) - return c.items, c.getSurrounding(), nil + return nil } - switch n := path[0].(type) { + switch n := c.path[0].(type) { case *ast.Ident: - if pgf.File.Name == n { - if err := c.packageNameCompletions(ctx, fh.URI(), n); err != nil { - return nil, nil, err - } - return c.items, c.getSurrounding(), nil - } else if sel, ok := path[1].(*ast.SelectorExpr); ok && sel.Sel == n { + if c.file.Name == n { + return c.packageNameCompletions(ctx, c.fh.URI(), n) + } else if sel, ok := c.path[1].(*ast.SelectorExpr); ok && sel.Sel == n { // Is this the Sel part of a selector? - if err := c.selector(ctx, sel); err != nil { - return nil, nil, err - } - } else if err := c.lexical(ctx); err != nil { - return nil, nil, err + return c.selector(ctx, sel) } + return c.lexical(ctx) // The function name hasn't been typed yet, but the parens are there: // recv.‸(arg) case *ast.TypeAssertExpr: // Create a fake selector expression. - if err := c.selector(ctx, &ast.SelectorExpr{X: n.X}); err != nil { - return nil, nil, err - } - + return c.selector(ctx, &ast.SelectorExpr{X: n.X}) case *ast.SelectorExpr: - if err := c.selector(ctx, n); err != nil { - return nil, nil, err - } - + return c.selector(ctx, n) // At the file scope, only keywords are allowed. case *ast.BadDecl, *ast.File: c.addKeywordCompletions() - default: // fallback to lexical completions - if err := c.lexical(ctx); err != nil { - return nil, nil, err - } + return c.lexical(ctx) } - // Statement candidates offer an entire statement in certain - // contexts, as opposed to a single object. Add statement candidates - // last because they depend on other candidates having already been - // collected. - c.addStatementCandidates() - - return c.items, c.getSurrounding(), nil + return nil } // containingIdent returns the *ast.Ident containing pos, if any. It @@ -788,7 +783,8 @@ func (c *completer) populateImportCompletions(ctx context.Context, searchImport obj := types.NewPkgName(0, nil, pkg.IdentName, types.NewPackage(pkgToConsider, pkg.IdentName)) cand := candidate{obj: obj, name: namePrefix + name + nameSuffix, score: score} // We use c.item here to be able to manually update the detail for a - // candidate. c.found doesn't give us access to the completion item. + // candidate. deepSearch doesn't give us access to the completion item, + // so we don't enqueue the item here. if item, err := c.item(ctx, cand); err == nil { item.Detail = fmt.Sprintf("%q", pkgToConsider) c.items = append(c.items, item) @@ -838,7 +834,7 @@ func (c *completer) populateCommentCompletions(ctx context.Context, comment *ast continue } obj := c.pkg.GetTypesInfo().ObjectOf(name) - c.found(ctx, candidate{obj: obj, score: stdScore}) + c.deepState.enqueue(&searchPath{}, candidate{obj: obj, score: stdScore}) } case *ast.TypeSpec: // add TypeSpec fields to completion @@ -865,8 +861,10 @@ func (c *completer) populateCommentCompletions(ctx context.Context, comment *ast score = highScore } - // we use c.item in addFieldItems so we have to use c.item here to ensure scoring - // order is maintained. c.found manipulates the score + // we use c.item in addFieldItems so we have to use c.item + // here to ensure scoring order is maintained. deepSearch + // manipulates the score so we can't enqueue the item + // directly. if item, err := c.item(ctx, candidate{obj: obj, name: obj.Name(), score: score}); err == nil { c.items = append(c.items, item) } @@ -897,8 +895,10 @@ func (c *completer) populateCommentCompletions(ctx context.Context, comment *ast } for i := 0; i < recvStruct.NumFields(); i++ { field := recvStruct.Field(i) - // we use c.item in addFieldItems so we have to use c.item here to ensure scoring - // order is maintained. c.found maniplulates the score + // we use c.item in addFieldItems so we have to + // use c.item here to ensure scoring order is + // maintained. deepSearch manipulates the score so + // we can't enqueue the items directly. item, err := c.item(ctx, candidate{obj: field, name: field.Name(), score: lowScore}) if err != nil { continue @@ -918,8 +918,8 @@ func (c *completer) populateCommentCompletions(ctx context.Context, comment *ast continue } - // We don't want to expandFuncCall inside comments. - // c.found() doesn't respect this setting + // We don't want to expandFuncCall inside comments. deepSearch + // doesn't respect this setting so we don't enqueue the item here. item, err := c.item(ctx, candidate{ obj: obj, name: obj.Name(), @@ -1005,8 +1005,8 @@ func (c *completer) addFieldItems(ctx context.Context, fields *ast.FieldList) { expandFuncCall: false, score: score, } - // We don't want to expandFuncCall inside comments. - // c.found() doesn't respect this setting + // We don't want to expandFuncCall inside comments. deepSearch + // doesn't respect this setting so we don't enqueue the item here. if item, err := c.item(ctx, cand); err == nil { c.items = append(c.items, item) } @@ -1042,7 +1042,7 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error { if pkgName, ok := c.pkg.GetTypesInfo().Uses[id].(*types.PkgName); ok { candidates := c.packageMembers(ctx, pkgName.Imported(), stdScore, nil) for _, cand := range candidates { - c.found(ctx, cand) + c.deepState.enqueue(&searchPath{}, cand) } return nil } @@ -1053,7 +1053,7 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error { if ok { candidates := c.methodsAndFields(ctx, tv.Type, tv.Addressable(), nil) for _, cand := range candidates { - c.found(ctx, cand) + c.deepState.enqueue(&searchPath{}, cand) } return nil } @@ -1110,7 +1110,7 @@ func (c *completer) unimportedMembers(ctx context.Context, id *ast.Ident) error } candidates := c.packageMembers(ctx, pkg.GetTypes(), unimportedScore(relevances[path]), imp) for _, cand := range candidates { - c.found(ctx, cand) + c.deepState.enqueue(&searchPath{}, cand) } if len(c.items) >= unimportedMemberTarget { return nil @@ -1132,7 +1132,7 @@ func (c *completer) unimportedMembers(ctx context.Context, id *ast.Ident) error pkg := types.NewPackage(pkgExport.Fix.StmtInfo.ImportPath, pkgExport.Fix.IdentName) for _, export := range pkgExport.Exports { score := unimportedScore(pkgExport.Fix.Relevance) - c.found(ctx, candidate{ + c.deepState.enqueue(&searchPath{}, candidate{ obj: types.NewVar(0, pkg, export, nil), score: score, imp: &importInfo{ @@ -1277,7 +1277,7 @@ func (c *completer) lexical(ctx context.Context) error { // If we haven't already added a candidate for an object with this name. if _, ok := seen[obj.Name()]; !ok { seen[obj.Name()] = struct{}{} - c.found(ctx, candidate{ + c.deepState.enqueue(&searchPath{}, candidate{ obj: obj, score: score, addressable: isVar(obj), @@ -1306,7 +1306,7 @@ func (c *completer) lexical(ctx context.Context) error { if imports.ImportPathToAssumedName(pkg.Path()) != pkg.Name() { imp.name = pkg.Name() } - c.found(ctx, candidate{ + c.deepState.enqueue(&searchPath{}, candidate{ obj: obj, score: stdScore, imp: imp, @@ -1345,7 +1345,7 @@ func (c *completer) lexical(ctx context.Context) error { // Make sure the type name matches before considering // candidate. This cuts down on useless candidates. if c.matchingTypeName(&fakeNamedType) { - c.found(ctx, fakeNamedType) + c.deepState.enqueue(&searchPath{}, fakeNamedType) } } } @@ -1405,7 +1405,7 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru if count >= maxUnimportedPackageNames { return nil } - c.found(ctx, candidate{ + c.deepState.enqueue(&searchPath{}, candidate{ obj: types.NewPkgName(0, nil, pkg.GetTypes().Name(), pkg.GetTypes()), score: unimportedScore(relevances[path]), imp: imp, @@ -1436,7 +1436,7 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru // multiple packages of the same name as completion suggestions, since // only one will be chosen. obj := types.NewPkgName(0, nil, pkg.IdentName, types.NewPackage(pkg.StmtInfo.ImportPath, pkg.IdentName)) - c.found(ctx, candidate{ + c.deepState.enqueue(&searchPath{}, candidate{ obj: obj, score: unimportedScore(pkg.Relevance), imp: &importInfo{ @@ -1498,7 +1498,7 @@ func (c *completer) structLiteralFieldName(ctx context.Context) error { for i := 0; i < t.NumFields(); i++ { field := t.Field(i) if !addedFields[field] { - c.found(ctx, candidate{ + c.deepState.enqueue(&searchPath{}, candidate{ obj: field, score: highScore, }) diff --git a/internal/lsp/source/completion/completion_labels.go b/internal/lsp/source/completion/completion_labels.go index 3c544129e3..ccdf9efc0c 100644 --- a/internal/lsp/source/completion/completion_labels.go +++ b/internal/lsp/source/completion/completion_labels.go @@ -58,7 +58,7 @@ func (c *completer) labels(ctx context.Context, lt labelType) { addLabel := func(score float64, l *ast.LabeledStmt) { labelObj := c.pkg.GetTypesInfo().ObjectOf(l.Label) if labelObj != nil { - c.found(ctx, candidate{obj: labelObj, score: score}) + c.deepState.enqueue(&searchPath{}, candidate{obj: labelObj, score: score}) } } diff --git a/internal/lsp/source/completion/deep_completion.go b/internal/lsp/source/completion/deep_completion.go index c7754ac51f..22ca2d0899 100644 --- a/internal/lsp/source/completion/deep_completion.go +++ b/internal/lsp/source/completion/deep_completion.go @@ -32,8 +32,7 @@ const MaxDeepCompletions = 3 // "deep completion" refers to searching into objects' fields and methods to // find more completion candidates. type deepCompletionState struct { - // enabled indicates wether deep completion is permitted. It should be - // reset to original value if manually disabled for an individual case. + // enabled indicates wether deep completion is permitted. enabled bool // queueClosed is used to disable adding new items to search queue once @@ -121,14 +120,6 @@ func (s *deepCompletionState) inDeepCompletion() bool { return len(s.curPath.path) > 0 } -// reset resets deepCompletionState since found might be called multiple times. -// We don't reset high scores since multiple calls to found still respect the -// same MaxDeepCompletions count. -func (s *deepCompletionState) reset() { - s.searchQueue = nil - s.curPath = &searchPath{} -} - // appendToSearchPath appends an object to a given searchPath. func appendToSearchPath(oldPath searchPath, obj types.Object, invoke bool) *searchPath { name := obj.Name() @@ -146,26 +137,23 @@ func appendToSearchPath(oldPath searchPath, obj types.Object, invoke bool) *sear } } -// found adds a candidate to completion items if it's a valid suggestion and -// searches the candidate's subordinate objects for more completion items if -// deep completion is enabled. -func (c *completer) found(ctx context.Context, cand candidate) { - // reset state at the end so current state doesn't affect completions done - // outside c.found. - defer c.deepState.reset() - - // At the top level, dedupe by object. - if c.seen[cand.obj] { - return - } - c.seen[cand.obj] = true - - c.deepState.enqueue(&searchPath{}, cand) +// deepSearch searches a candidate and its subordinate objects for completion +// items if deep completion is enabled and adds the valid candidates to +// completion items. +func (c *completer) deepSearch(ctx context.Context) { outer: for len(c.deepState.searchQueue) > 0 { item := c.deepState.dequeue() - curCand := item.cand - obj := curCand.obj + cand := item.cand + obj := cand.obj + + // At the top level, dedupe by object. + if len(item.searchPath.path) == 0 { + if c.seen[cand.obj] { + continue + } + c.seen[cand.obj] = true + } // If obj is not accessible because it lives in another package and is // not exported, don't treat it as a completion candidate. @@ -194,7 +182,7 @@ outer: // update tracked current path since other functions might check it. c.deepState.curPath = item.searchPath - c.addCandidate(ctx, curCand) + c.addCandidate(ctx, cand) c.deepState.candidateCount++ if c.opts.budget > 0 && c.deepState.candidateCount%100 == 0 { @@ -235,7 +223,7 @@ outer: if sig.Params().Len() == 0 && sig.Results().Len() == 1 { newSearchPath := appendToSearchPath(*item.searchPath, obj, true) // The result of a function call is not addressable. - candidates := c.methodsAndFields(ctx, sig.Results().At(0).Type(), false, curCand.imp) + candidates := c.methodsAndFields(ctx, sig.Results().At(0).Type(), false, cand.imp) c.deepState.enqueue(newSearchPath, candidates...) } } @@ -243,10 +231,10 @@ outer: newSearchPath := appendToSearchPath(*item.searchPath, obj, false) switch obj := obj.(type) { case *types.PkgName: - candidates := c.packageMembers(ctx, obj.Imported(), stdScore, curCand.imp) + candidates := c.packageMembers(ctx, obj.Imported(), stdScore, cand.imp) c.deepState.enqueue(newSearchPath, candidates...) default: - candidates := c.methodsAndFields(ctx, obj.Type(), curCand.addressable, curCand.imp) + candidates := c.methodsAndFields(ctx, obj.Type(), cand.addressable, cand.imp) c.deepState.enqueue(newSearchPath, candidates...) } }