internal/lsp: add completion suggestions for import statements
This change adds completion within import blocks. Completions are suggested by directory depth of import so end user isn't shown a large list of possible imports at once. As an example, searching import for prefix "golang" would suggest "golang.org/" and then subdirectories under that (ex: "golang.org/x/"") on successive completion request and so on until a complete package path is selected.
Change-Id: I962d32f2b7eef2c6b2ce8dc8a326ea34c726aa36
Reviewed-on: https://go-review.googlesource.com/c/tools/+/250301
Run-TryBot: Danish Dua <danishdua@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/imports/fix.go b/internal/imports/fix.go
index 62d9fe8..613afc4 100644
--- a/internal/imports/fix.go
+++ b/internal/imports/fix.go
@@ -615,7 +615,7 @@
packageName: path.Base(importPath),
relevance: MaxRelevance,
}
- if notSelf(p) && wrappedCallback.packageNameLoaded(p) {
+ if notSelf(p) && wrappedCallback.dirFound(p) && wrappedCallback.packageNameLoaded(p) {
wrappedCallback.exportsLoaded(p, exports)
}
}
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index e539461..7a08f52 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -492,7 +492,12 @@
// Check if completion at this position is valid. If not, return early.
switch n := path[0].(type) {
case *ast.BasicLit:
- // Skip completion inside any kind of literal.
+ // Skip completion inside literals except for ImportSpec
+ if len(path) > 1 {
+ if _, ok := path[1].(*ast.ImportSpec); ok {
+ break
+ }
+ }
return nil, nil, nil
case *ast.CallExpr:
if n.Ellipsis.IsValid() && pos > n.Ellipsis && pos <= n.Ellipsis+token.Pos(len("...")) {
@@ -567,7 +572,18 @@
defer c.sortItems()
- // If we're inside a comment return comment completions
+ // 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()) {
+ continue
+ }
+ if err := c.populateImportCompletions(ctx, importSpec); err != nil {
+ return nil, nil, err
+ }
+ return c.items, c.getSurrounding(), nil
+ }
+
+ // 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() {
// deep completion doesn't work properly in comments since we don't
@@ -735,7 +751,91 @@
}
}
-// populateCommentCompletions yields completions for comments preceding or in declarations
+// populateImportCompletions yields completions for an import path around the cursor.
+//
+// Completions are suggested at the directory depth of the given import path so
+// that we don't overwhelm the user with a large list of possibilities. As an
+// example, a completion for the prefix "golang" results in "golang.org/".
+// Completions for "golang.org/" yield its subdirectories
+// (i.e. "golang.org/x/"). The user is meant to accept completion suggestions
+// until they reach a complete import path.
+func (c *completer) populateImportCompletions(ctx context.Context, searchImport *ast.ImportSpec) error {
+ c.surrounding = &Selection{
+ content: searchImport.Path.Value,
+ cursor: c.pos,
+ mappedRange: newMappedRange(c.snapshot.FileSet(), c.mapper, searchImport.Path.Pos(), searchImport.Path.End()),
+ }
+
+ seenImports := make(map[string]struct{})
+ for _, importSpec := range c.file.Imports {
+ if importSpec.Path.Value == searchImport.Path.Value {
+ continue
+ }
+ importPath, err := strconv.Unquote(importSpec.Path.Value)
+ if err != nil {
+ return err
+ }
+ seenImports[importPath] = struct{}{}
+ }
+
+ prefixEnd := c.pos - searchImport.Path.ValuePos
+ // Extract the text between the quotes (if any) in an import spec.
+ // prefix is the part of import path before the cursor.
+ prefix := strings.Trim(searchImport.Path.Value[:prefixEnd], `"`)
+
+ // The number of directories in the import path gives us the depth at
+ // which to search.
+ depth := len(strings.Split(prefix, "/")) - 1
+
+ var mu sync.Mutex // guard c.items locally, since searchImports is called in parallel
+ seen := make(map[string]struct{})
+ searchImports := func(pkg imports.ImportFix) {
+ path := pkg.StmtInfo.ImportPath
+ if _, ok := seenImports[path]; ok {
+ return
+ }
+
+ // Any package path containing fewer directories than the search
+ // prefix is not a match.
+ pkgDirList := strings.Split(path, "/")
+ if len(pkgDirList) < depth+1 {
+ return
+ }
+ pkgToConsider := strings.Join(pkgDirList[:depth+1], "/")
+
+ score := float64(pkg.Relevance)
+ if len(pkgDirList)-1 == depth {
+ score *= highScore
+ } else {
+ // For incomplete package paths, add a terminal slash to indicate that the
+ // user should keep triggering completions.
+ pkgToConsider += "/"
+ }
+
+ if _, ok := seen[pkgToConsider]; ok {
+ return
+ }
+ seen[pkgToConsider] = struct{}{}
+
+ mu.Lock()
+ defer mu.Unlock()
+
+ obj := types.NewPkgName(0, nil, pkg.IdentName, types.NewPackage(pkgToConsider, pkg.IdentName))
+ // Running goimports logic in completions is expensive, and the
+ // (*completer).found method imposes a 100ms budget. Work-around this
+ // by adding to c.items directly.
+ cand := candidate{obj: obj, name: `"` + pkgToConsider + `"`, score: score}
+ if item, err := c.item(ctx, cand); err == nil {
+ c.items = append(c.items, item)
+ }
+ }
+
+ return c.snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
+ return imports.GetImportPaths(ctx, searchImports, prefix, c.filename, c.pkg.GetTypes().Name(), opts.Env)
+ })
+}
+
+// populateCommentCompletions yields completions for comments preceding or in declarations.
func (c *completer) populateCommentCompletions(ctx context.Context, comment *ast.CommentGroup) {
// If the completion was triggered by a period, ignore it. These types of
// completions will not be useful in comments.
diff --git a/internal/lsp/testdata/lsp/primarymod/importedcomplit/imported_complit.go b/internal/lsp/testdata/lsp/primarymod/importedcomplit/imported_complit.go
deleted file mode 100644
index 99424ec..0000000
--- a/internal/lsp/testdata/lsp/primarymod/importedcomplit/imported_complit.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package importedcomplit
-
-import (
- "golang.org/x/tools/internal/lsp/foo"
-)
-
-func _() {
- var V int //@item(icVVar, "V", "int", "var")
- _ = foo.StructFoo{V} //@complete("}", Value, icVVar)
-}
-
-func _() {
- var (
- aa string //@item(icAAVar, "aa", "string", "var")
- ab int //@item(icABVar, "ab", "int", "var")
- )
-
- _ = foo.StructFoo{a} //@complete("}", abVar, aaVar)
-
- var s struct {
- AA string //@item(icFieldAA, "AA", "string", "field")
- AB int //@item(icFieldAB, "AB", "int", "field")
- }
-
- _ = foo.StructFoo{s.} //@complete("}", icFieldAB, icFieldAA)
-}
diff --git a/internal/lsp/testdata/lsp/primarymod/importedcomplit/imported_complit.go.in b/internal/lsp/testdata/lsp/primarymod/importedcomplit/imported_complit.go.in
new file mode 100644
index 0000000..dddf20d
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/importedcomplit/imported_complit.go.in
@@ -0,0 +1,41 @@
+package importedcomplit
+
+import (
+ "golang.org/x/tools/internal/lsp/foo"
+
+ // import completions
+ "fm" //@complete("\" //", fmtImport)
+ "go/pars" //@complete("\" //", parserImport)
+ "golang.org/x/tools/internal/lsp/signa" //@complete("na\" //", signatureImport)
+ "golang.org/x/too" //@complete("\" //", toolsImport)
+ "crypto/elli" //@complete("\" //", cryptoImport)
+ "golang.org/x/tools/internal/lsp/sign" //@complete("\" //", signatureImport)
+ namedParser "go/pars" //@complete("\" //", parserImport)
+)
+
+func _() {
+ var V int //@item(icVVar, "V", "int", "var")
+ _ = foo.StructFoo{V} //@complete("}", Value, icVVar)
+}
+
+func _() {
+ var (
+ aa string //@item(icAAVar, "aa", "string", "var")
+ ab int //@item(icABVar, "ab", "int", "var")
+ )
+
+ _ = foo.StructFoo{a} //@complete("}", abVar, aaVar)
+
+ var s struct {
+ AA string //@item(icFieldAA, "AA", "string", "field")
+ AB int //@item(icFieldAB, "AB", "int", "field")
+ }
+
+ _ = foo.StructFoo{s.} //@complete("}", icFieldAB, icFieldAA)
+}
+
+/* "fmt" */ //@item(fmtImport, "\"fmt\"", "\"fmt\"", "package")
+/* "go/parser" */ //@item(parserImport, "\"go/parser\"", "\"go/parser\"", "package")
+/* "golang.org/x/tools/internal/lsp/signature" */ //@item(signatureImport, "\"golang.org/x/tools/internal/lsp/signature\"", "\"golang.org/x/tools/internal/lsp/signature\"", "package")
+/* "golang.org/x/tools/" */ //@item(toolsImport, "\"golang.org/x/tools/\"", "\"golang.org/x/tools/\"", "package")
+/* "crypto/elliptic" */ //@item(cryptoImport, "\"crypto/elliptic\"", "\"crypto/elliptic\"", "package")
diff --git a/internal/lsp/testdata/lsp/summary.txt.golden b/internal/lsp/testdata/lsp/summary.txt.golden
index 36120de..416c549 100644
--- a/internal/lsp/testdata/lsp/summary.txt.golden
+++ b/internal/lsp/testdata/lsp/summary.txt.golden
@@ -1,7 +1,7 @@
-- summary --
CallHierarchyCount = 1
CodeLensCount = 5
-CompletionsCount = 247
+CompletionsCount = 254
CompletionSnippetCount = 85
UnimportedCompletionsCount = 6
DeepCompletionsCount = 5