Skip to content

Commit cf97e2b

Browse files
committed
internal/lsp: add package completion suggestions
This changes add package completions suggestions for new files. Package suggestions are other packages used in the same directory, test packages for those packages, the package 'main' and the directory name. Fixes golang/go#34008 Change-Id: I69922e0cb0787e82eebe505618c3c07aa48859e6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/251160 Run-TryBot: Danish Dua <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent 80e1b03 commit cf97e2b

File tree

4 files changed

+317
-4
lines changed

4 files changed

+317
-4
lines changed

internal/lsp/fake/editor.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,30 @@ func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens
788788
return lens, nil
789789
}
790790

791+
// Completion executes a completion request on the server.
792+
func (e *Editor) Completion(ctx context.Context, path string, pos Pos) (*protocol.CompletionList, error) {
793+
if e.Server == nil {
794+
return nil, nil
795+
}
796+
e.mu.Lock()
797+
_, ok := e.buffers[path]
798+
e.mu.Unlock()
799+
if !ok {
800+
return nil, fmt.Errorf("buffer %q is not open", path)
801+
}
802+
params := &protocol.CompletionParams{
803+
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
804+
TextDocument: e.textDocumentIdentifier(path),
805+
Position: pos.ToProtocolPosition(),
806+
},
807+
}
808+
completions, err := e.Server.Completion(ctx, params)
809+
if err != nil {
810+
return nil, err
811+
}
812+
return completions, nil
813+
}
814+
791815
// References executes a reference request on the server.
792816
func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) {
793817
if e.Server == nil {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package regtest
6+
7+
import (
8+
"fmt"
9+
"testing"
10+
11+
"golang.org/x/tools/internal/lsp/fake"
12+
"golang.org/x/tools/internal/lsp/protocol"
13+
)
14+
15+
func TestPackageCompletion(t *testing.T) {
16+
const files = `
17+
-- go.mod --
18+
module mod.com
19+
20+
-- fruits/apple.go --
21+
package apple
22+
23+
fun apple() int {
24+
return 0
25+
}
26+
27+
-- fruits/testfile.go --`
28+
29+
want := []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}
30+
run(t, files, func(t *testing.T, env *Env) {
31+
env.OpenFile("fruits/testfile.go")
32+
content := env.ReadWorkspaceFile("fruits/testfile.go")
33+
if content != "" {
34+
t.Fatal("testfile.go should be empty to test completion on end of file without newline")
35+
}
36+
37+
completions, err := env.Editor.Completion(env.Ctx, "fruits/testfile.go", fake.Pos{
38+
Line: 0,
39+
Column: 0,
40+
})
41+
if err != nil {
42+
t.Fatal(err)
43+
}
44+
45+
diff := compareCompletionResults(want, completions.Items)
46+
if diff != "" {
47+
t.Fatal(diff)
48+
}
49+
})
50+
}
51+
52+
func TestPackageNameCompletion(t *testing.T) {
53+
const files = `
54+
-- go.mod --
55+
module mod.com
56+
57+
-- math/add.go --
58+
package ma
59+
`
60+
61+
want := []string{"ma", "ma_test", "main", "math", "math_test"}
62+
run(t, files, func(t *testing.T, env *Env) {
63+
env.OpenFile("math/add.go")
64+
completions, err := env.Editor.Completion(env.Ctx, "math/add.go", fake.Pos{
65+
Line: 0,
66+
Column: 10,
67+
})
68+
if err != nil {
69+
t.Fatal(err)
70+
}
71+
72+
diff := compareCompletionResults(want, completions.Items)
73+
if diff != "" {
74+
t.Fatal(diff)
75+
}
76+
})
77+
}
78+
79+
func compareCompletionResults(want []string, gotItems []protocol.CompletionItem) string {
80+
if len(gotItems) != len(want) {
81+
return fmt.Sprintf("got %v completion(s), want %v", len(gotItems), len(want))
82+
}
83+
84+
var got []string
85+
for _, item := range gotItems {
86+
got = append(got, item.Label)
87+
}
88+
89+
for i, v := range got {
90+
if v != want[i] {
91+
return fmt.Sprintf("completion results are not the same: got %v, want %v", got, want)
92+
}
93+
}
94+
95+
return ""
96+
}

internal/lsp/source/completion.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -469,8 +469,19 @@ func Completion(ctx context.Context, snapshot Snapshot, fh FileHandle, protoPos
469469
startTime := time.Now()
470470

471471
pkg, pgf, err := getParsedFile(ctx, snapshot, fh, NarrowestPackage)
472-
if err != nil {
473-
return nil, nil, errors.Errorf("getting file for Completion: %w", err)
472+
if err != nil || pgf.File.Package == token.NoPos {
473+
// If we can't parse this file or find position for the package
474+
// keyword, it may be missing a package declaration. Try offering
475+
// suggestions for the package declaration.
476+
// Note that this would be the case even if the keyword 'package' is
477+
// present but no package name exists.
478+
items, surrounding, innerErr := packageClauseCompletions(ctx, snapshot, fh, protoPos)
479+
if innerErr != nil {
480+
// return the error for getParsedFile since it's more relevant in this situation.
481+
return nil, nil, errors.Errorf("getting file for Completion: %w", err)
482+
483+
}
484+
return items, surrounding, nil
474485
}
475486
spn, err := pgf.Mapper.PointSpan(protoPos)
476487
if err != nil {
@@ -510,6 +521,9 @@ func Completion(ctx context.Context, snapshot Snapshot, fh FileHandle, protoPos
510521
if obj, ok := pkg.GetTypesInfo().Defs[n]; ok {
511522
if v, ok := obj.(*types.Var); ok && v.IsField() && v.Embedded() {
512523
// An anonymous field is also a reference to a type.
524+
} else if pgf.File.Name == n {
525+
// Don't skip completions if Ident is for package name.
526+
break
513527
} else {
514528
objStr := ""
515529
if obj != nil {
@@ -615,8 +629,13 @@ func Completion(ctx context.Context, snapshot Snapshot, fh FileHandle, protoPos
615629

616630
switch n := path[0].(type) {
617631
case *ast.Ident:
618-
// Is this the Sel part of a selector?
619-
if sel, ok := path[1].(*ast.SelectorExpr); ok && sel.Sel == n {
632+
if pgf.File.Name == n {
633+
if err := c.packageNameCompletions(ctx, fh.URI(), n); err != nil {
634+
return nil, nil, err
635+
}
636+
return c.items, c.getSurrounding(), nil
637+
} else if sel, ok := path[1].(*ast.SelectorExpr); ok && sel.Sel == n {
638+
// Is this the Sel part of a selector?
620639
if err := c.selector(ctx, sel); err != nil {
621640
return nil, nil, err
622641
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package source
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"go/ast"
11+
"go/types"
12+
"path/filepath"
13+
"strings"
14+
15+
"golang.org/x/tools/internal/lsp/fuzzy"
16+
"golang.org/x/tools/internal/lsp/protocol"
17+
"golang.org/x/tools/internal/span"
18+
errors "golang.org/x/xerrors"
19+
)
20+
21+
// packageClauseCompletions offers completions for a package declaration when
22+
// one is not present in the given file.
23+
func packageClauseCompletions(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]CompletionItem, *Selection, error) {
24+
// We know that the AST for this file will be empty due to the missing
25+
// package declaration, but parse it anyway to get a mapper.
26+
pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader)
27+
if err != nil {
28+
return nil, nil, err
29+
}
30+
31+
// Check that the file is completely empty, to avoid offering incorrect package
32+
// clause completions.
33+
// TODO: Support package clause completions in all files.
34+
if pgf.Tok.Size() != 0 {
35+
return nil, nil, errors.New("package clause completion is only offered for empty file")
36+
}
37+
38+
cursorSpan, err := pgf.Mapper.PointSpan(pos)
39+
if err != nil {
40+
return nil, nil, err
41+
}
42+
rng, err := cursorSpan.Range(pgf.Mapper.Converter)
43+
if err != nil {
44+
return nil, nil, err
45+
}
46+
47+
surrounding := &Selection{
48+
content: "",
49+
cursor: rng.Start,
50+
mappedRange: newMappedRange(snapshot.FileSet(), pgf.Mapper, rng.Start, rng.Start),
51+
}
52+
53+
packageSuggestions, err := packageSuggestions(ctx, snapshot, fh.URI(), "")
54+
if err != nil {
55+
return nil, nil, err
56+
}
57+
58+
var items []CompletionItem
59+
for _, pkg := range packageSuggestions {
60+
insertText := fmt.Sprintf("package %s", pkg.name)
61+
items = append(items, CompletionItem{
62+
Label: insertText,
63+
Kind: protocol.ModuleCompletion,
64+
InsertText: insertText,
65+
Score: pkg.score,
66+
})
67+
}
68+
69+
return items, surrounding, nil
70+
}
71+
72+
// packageNameCompletions returns name completions for a package clause using
73+
// the current name as prefix.
74+
func (c *completer) packageNameCompletions(ctx context.Context, fileURI span.URI, name *ast.Ident) error {
75+
cursor := int(c.pos - name.NamePos)
76+
if cursor < 0 || cursor > len(name.Name) {
77+
return errors.New("cursor is not in package name identifier")
78+
}
79+
80+
prefix := name.Name[:cursor]
81+
packageSuggestions, err := packageSuggestions(ctx, c.snapshot, fileURI, prefix)
82+
if err != nil {
83+
return err
84+
}
85+
86+
for _, pkg := range packageSuggestions {
87+
if item, err := c.item(ctx, pkg); err == nil {
88+
c.items = append(c.items, item)
89+
}
90+
}
91+
return nil
92+
}
93+
94+
// packageSuggestions returns a list of packages from workspace packages that
95+
// have the given prefix and are used in the the same directory as the given
96+
// file. This also includes test packages for these packages (<pkg>_test) and
97+
// the directory name itself.
98+
func packageSuggestions(ctx context.Context, snapshot Snapshot, fileURI span.URI, prefix string) ([]candidate, error) {
99+
workspacePackages, err := snapshot.WorkspacePackages(ctx)
100+
if err != nil {
101+
return nil, err
102+
}
103+
104+
dirPath := filepath.Dir(string(fileURI))
105+
dirName := filepath.Base(dirPath)
106+
107+
seenPkgs := make(map[string]struct{})
108+
109+
toCandidate := func(name string, score float64) candidate {
110+
obj := types.NewPkgName(0, nil, name, types.NewPackage("", name))
111+
return candidate{obj: obj, name: name, score: score}
112+
}
113+
114+
matcher := fuzzy.NewMatcher(prefix)
115+
116+
// The `go` command by default only allows one package per directory but we
117+
// support multiple package suggestions since gopls is build system agnostic.
118+
var packages []candidate
119+
for _, pkg := range workspacePackages {
120+
if pkg.Name() == "main" {
121+
continue
122+
}
123+
if _, ok := seenPkgs[pkg.Name()]; ok {
124+
continue
125+
}
126+
127+
// Only add packages that are previously used in the current directory.
128+
var relevantPkg bool
129+
for _, pgf := range pkg.CompiledGoFiles() {
130+
if filepath.Dir(string(pgf.URI)) == dirPath {
131+
relevantPkg = true
132+
break
133+
}
134+
}
135+
if !relevantPkg {
136+
continue
137+
}
138+
139+
// Add a found package used in current directory as a high relevance
140+
// suggestion and the test package for it as a medium relevance
141+
// suggestion.
142+
if score := float64(matcher.Score(pkg.Name())); score > 0 {
143+
packages = append(packages, toCandidate(pkg.Name(), score*highScore))
144+
}
145+
seenPkgs[pkg.Name()] = struct{}{}
146+
147+
testPkgName := pkg.Name() + "_test"
148+
if _, ok := seenPkgs[testPkgName]; ok || strings.HasSuffix(pkg.Name(), "_test") {
149+
continue
150+
}
151+
if score := float64(matcher.Score(testPkgName)); score > 0 {
152+
packages = append(packages, toCandidate(testPkgName, score*stdScore))
153+
}
154+
seenPkgs[testPkgName] = struct{}{}
155+
}
156+
157+
// Add current directory name as a low relevance suggestion.
158+
if _, ok := seenPkgs[dirName]; !ok {
159+
if score := float64(matcher.Score(dirName)); score > 0 {
160+
packages = append(packages, toCandidate(dirName, score*lowScore))
161+
}
162+
163+
testDirName := dirName + "_test"
164+
if score := float64(matcher.Score(testDirName)); score > 0 {
165+
packages = append(packages, toCandidate(testDirName, score*lowScore))
166+
}
167+
}
168+
169+
if score := float64(matcher.Score("main")); score > 0 {
170+
packages = append(packages, toCandidate("main", score*lowScore))
171+
}
172+
173+
return packages, nil
174+
}

0 commit comments

Comments
 (0)