internal/lsp: add completions of unimported std lib pkgs

Unimported packages may be suggested as completion items. Since these
are not yet imported, they should be ranked lower than other candidates.

They also require an additional import statement to be valid, which is
provided as an AdditionalTextEdit.

Adding this import does not use astutil.AddNamedImport, to avoid
editing the current ast and work even if there are errors. Additionally,
it can be hard to determine what changes need to be made to the source
document from the ast, as astutil.AddNamedImport includes a merging
pass. Instead, the completion item simply adds another import
declaration.

Change-Id: Icbde226d843bd49ee3713cafcbd5299d51530695
Reviewed-on: https://go-review.googlesource.com/c/tools/+/190338
Run-TryBot: Suzy Mueller <suzmue@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go
index 6df9778..79f9aca 100644
--- a/internal/lsp/completion.go
+++ b/internal/lsp/completion.go
@@ -35,6 +35,7 @@
 		DeepComplete:          s.useDeepCompletions,
 		WantDocumentaton:      s.wantCompletionDocumentation,
 		WantFullDocumentation: s.hoverKind == fullDocumentation,
+		WantUnimported:        s.wantUnimportedCompletions,
 	})
 	if err != nil {
 		log.Print(ctx, "no completions found", tag.Of("At", rng), tag.Of("Failure", err))
@@ -96,7 +97,11 @@
 		if s.insertTextFormat == protocol.SnippetTextFormat {
 			insertText = candidate.Snippet(s.usePlaceholders)
 		}
-
+		addlEdits, err := ToProtocolEdits(m, candidate.AdditionalTextEdits)
+		if err != nil {
+			log.Error(ctx, "failed to convert to protocol edits", err)
+			continue
+		}
 		item := protocol.CompletionItem{
 			Label:  candidate.Label,
 			Detail: candidate.Detail,
@@ -105,7 +110,8 @@
 				NewText: insertText,
 				Range:   insertionRange,
 			},
-			InsertTextFormat: s.insertTextFormat,
+			InsertTextFormat:    s.insertTextFormat,
+			AdditionalTextEdits: addlEdits,
 			// This is a hack so that the client sorts completion results in the order
 			// according to their score. This can be removed upon the resolution of
 			// https://github.com/Microsoft/language-server-protocol/issues/348.
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index 738866d..0da3020 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -261,6 +261,10 @@
 	if useDeepCompletions, ok := c["useDeepCompletions"].(bool); ok {
 		s.useDeepCompletions = useDeepCompletions
 	}
+	// Check if want unimported package completions.
+	if wantUnimportedCompletions, ok := c["wantUnimportedCompletions"].(bool); ok {
+		s.wantUnimportedCompletions = wantUnimportedCompletions
+	}
 	return nil
 }
 
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 56db483..79b542d 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -100,6 +100,7 @@
 func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests.CompletionSnippets, items tests.CompletionItems) {
 	defer func() {
 		r.server.useDeepCompletions = false
+		r.server.wantUnimportedCompletions = false
 		r.server.wantCompletionDocumentation = false
 	}()
 
@@ -112,6 +113,7 @@
 		}
 
 		r.server.useDeepCompletions = strings.Contains(string(src.URI()), "deepcomplete")
+		r.server.wantUnimportedCompletions = strings.Contains(string(src.URI()), "unimported")
 
 		list := r.runCompletion(t, src)
 
@@ -141,6 +143,7 @@
 
 		for src, want := range snippets {
 			r.server.useDeepCompletions = strings.Contains(string(src.URI()), "deepcomplete")
+			r.server.wantUnimportedCompletions = strings.Contains(string(src.URI()), "unimported")
 
 			list := r.runCompletion(t, src)
 
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 5bcc844..9c5aea9 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -82,6 +82,7 @@
 	hoverKind                     hoverKind
 	useDeepCompletions            bool
 	wantCompletionDocumentation   bool
+	wantUnimportedCompletions     bool
 	insertTextFormat              protocol.InsertTextFormat
 	configurationSupported        bool
 	dynamicConfigurationSupported bool
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index 8178f04..762195b 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -12,6 +12,7 @@
 	"strings"
 
 	"golang.org/x/tools/go/ast/astutil"
+	"golang.org/x/tools/internal/imports"
 	"golang.org/x/tools/internal/lsp/fuzzy"
 	"golang.org/x/tools/internal/lsp/snippet"
 	"golang.org/x/tools/internal/span"
@@ -34,6 +35,14 @@
 
 	Kind CompletionItemKind
 
+	// An optional array of additional TextEdits that are applied when
+	// selecting this completion.
+	//
+	// Additional text edits should be used to change text unrelated to the current cursor position
+	// (for example adding an import statement at the top of the file if the completion item will
+	// insert an unqualified type).
+	AdditionalTextEdits []TextEdit
+
 	// Depth is how many levels were searched to find this completion.
 	// For example when completing "foo<>", "fooBar" is depth 0, and
 	// "fooBar.Baz" is depth 1.
@@ -146,6 +155,12 @@
 	// ctx is the context associated with this completion request.
 	ctx context.Context
 
+	// filename is the name of the file associated with this completion request.
+	filename string
+
+	// file is the AST of the file associated with this completion request.
+	file *ast.File
+
 	// pos is the position at which the request was triggered.
 	pos token.Pos
 
@@ -237,7 +252,7 @@
 
 // found adds a candidate completion. We will also search through the object's
 // members for more candidates.
-func (c *completer) found(obj types.Object, score float64) error {
+func (c *completer) found(obj types.Object, score float64, imp *imports.ImportInfo) error {
 	if obj.Pkg() != nil && obj.Pkg() != c.types && !obj.Exported() {
 		return errors.Errorf("%s is inaccessible from %s", obj.Name(), c.types.Path())
 	}
@@ -262,6 +277,7 @@
 	cand := candidate{
 		obj:   obj,
 		score: score,
+		imp:   imp,
 	}
 
 	if c.matchingType(&cand) {
@@ -301,12 +317,17 @@
 	// expandFuncCall is true if obj should be invoked in the completion.
 	// For example, expandFuncCall=true yields "foo()", expandFuncCall=false yields "foo".
 	expandFuncCall bool
+
+	// imp is the import that needs to be added to this package in order
+	// for this candidate to be valid. nil if no import needed.
+	imp *imports.ImportInfo
 }
 
 type CompletionOptions struct {
 	DeepComplete          bool
 	WantDocumentaton      bool
 	WantFullDocumentation bool
+	WantUnimported        bool
 }
 
 // Completion returns a list of possible candidates for completion, given a
@@ -351,6 +372,8 @@
 		qf:                        qualifier(file, pkg.GetTypes(), pkg.GetTypesInfo()),
 		view:                      view,
 		ctx:                       ctx,
+		filename:                  f.URI().Filename(),
+		file:                      file,
 		path:                      path,
 		pos:                       pos,
 		seen:                      make(map[types.Object]bool),
@@ -469,7 +492,7 @@
 func (c *completer) packageMembers(pkg *types.PkgName) {
 	scope := pkg.Imported().Scope()
 	for _, name := range scope.Names() {
-		c.found(scope.Lookup(name), stdScore)
+		c.found(scope.Lookup(name), stdScore, nil)
 	}
 }
 
@@ -485,12 +508,12 @@
 	}
 
 	for i := 0; i < mset.Len(); i++ {
-		c.found(mset.At(i).Obj(), stdScore)
+		c.found(mset.At(i).Obj(), stdScore, nil)
 	}
 
 	// Add fields of T.
 	for _, f := range fieldSelections(typ) {
-		c.found(f, stdScore)
+		c.found(f, stdScore, nil)
 	}
 	return nil
 }
@@ -550,7 +573,27 @@
 			// 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(obj, score)
+				c.found(obj, score, nil)
+			}
+		}
+	}
+
+	if c.opts.WantUnimported {
+		// Suggest packages that have not been imported yet.
+		pkgs, err := CandidateImports(c.ctx, c.view, c.filename)
+		if err != nil {
+			return err
+		}
+		score := stdScore
+		// Rank unimported packages significantly lower than other results.
+		score *= 0.07
+		for _, pkg := range pkgs {
+			if _, ok := seen[pkg.IdentName]; !ok {
+				// Do not add the unimported packages to seen, since we can have
+				// 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(obj, score, &pkg.StmtInfo)
 			}
 		}
 	}
@@ -585,7 +628,7 @@
 		for i := 0; i < t.NumFields(); i++ {
 			field := t.Field(i)
 			if !addedFields[field] {
-				c.found(field, highScore)
+				c.found(field, highScore, nil)
 			}
 		}
 
diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go
index 7cb4426..bacd578 100644
--- a/internal/lsp/source/completion_format.go
+++ b/internal/lsp/source/completion_format.go
@@ -35,6 +35,7 @@
 		kind               CompletionItemKind
 		plainSnippet       *snippet.Builder
 		placeholderSnippet *snippet.Builder
+		addlEdits          []TextEdit
 	)
 
 	// expandFuncCall mutates the completion label, detail, and snippets
@@ -85,16 +86,27 @@
 		detail = fmt.Sprintf("%q", obj.Imported().Path())
 	}
 
+	// If this candidate needs an additional import statement,
+	// add the additional text edits needed.
+	if cand.imp != nil {
+		edit, err := AddNamedImport(c.view.Session().Cache().FileSet(), c.file, cand.imp.Name, cand.imp.ImportPath)
+		if err != nil {
+			return CompletionItem{}, err
+		}
+		addlEdits = append(addlEdits, edit...)
+	}
+
 	detail = strings.TrimPrefix(detail, "untyped ")
 	item := CompletionItem{
-		Label:              label,
-		InsertText:         insert,
-		Detail:             detail,
-		Kind:               kind,
-		Score:              cand.score,
-		Depth:              len(c.deepState.chain),
-		plainSnippet:       plainSnippet,
-		placeholderSnippet: placeholderSnippet,
+		Label:               label,
+		InsertText:          insert,
+		AdditionalTextEdits: addlEdits,
+		Detail:              detail,
+		Kind:                kind,
+		Score:               cand.score,
+		Depth:               len(c.deepState.chain),
+		plainSnippet:        plainSnippet,
+		placeholderSnippet:  placeholderSnippet,
 	}
 	// TODO(rstambler): Log errors when this feature is enabled.
 	if c.opts.WantDocumentaton {
diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go
index 1e260be..9141869 100644
--- a/internal/lsp/source/format.go
+++ b/internal/lsp/source/format.go
@@ -175,6 +175,35 @@
 	return edits, editsPerFix, nil
 }
 
+// AllImportsFixes formats f for each possible fix to the imports.
+// In addition to returning the result of applying all edits,
+// it returns a list of fixes that could be applied to the file, with the
+// corresponding TextEdits that would be needed to apply that fix.
+func CandidateImports(ctx context.Context, view View, filename string) (pkgs []imports.ImportFix, err error) {
+	ctx, done := trace.StartSpan(ctx, "source.CandidateImports")
+	defer done()
+
+	options := &imports.Options{
+		// Defaults.
+		AllErrors:  true,
+		Comments:   true,
+		Fragment:   true,
+		FormatOnly: false,
+		TabIndent:  true,
+		TabWidth:   8,
+	}
+	importFn := func(opts *imports.Options) error {
+		pkgs, err = imports.GetAllCandidates(filename, opts)
+		return err
+	}
+	err = view.RunProcessEnvFunc(ctx, importFn, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return pkgs, nil
+}
+
 // hasParseErrors returns true if the given file has parse errors.
 func hasParseErrors(pkg Package, uri span.URI) bool {
 	for _, err := range pkg.GetErrors() {
diff --git a/internal/lsp/source/imports.go b/internal/lsp/source/imports.go
new file mode 100644
index 0000000..0f3e3fd
--- /dev/null
+++ b/internal/lsp/source/imports.go
@@ -0,0 +1,133 @@
+// Copyright 2019 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 source
+
+import (
+	"bytes"
+	"go/ast"
+	"go/format"
+	"go/token"
+	"strconv"
+
+	"golang.org/x/tools/internal/span"
+)
+
+// Taken and then modified from golang.org/x/tools/go/ast/astutil.
+//
+// We currently choose to create our own version of AddNamedImport for the following reasons:
+// 	1. 	We do not want to edit the current ast. This is so that we can use the same ast
+// 		to get the changes from multiple distinct modifications.
+//  2.	We need the changes that *only* affect the import declarations, because the edits
+// 		are not allowed to overlap with the position in the source that is being edited.
+//		astutil.AddNamedImport makes it hard to determine what changes need to be made
+//		to the source document from the ast, as astutil.AddNamedImport includes a merging pass.
+
+// AddNamedImport adds the import with the given name and path to the file f, if absent.
+// If name is not empty, it is used to rename the import.
+//
+// For example, calling
+//	AddNamedImport(fset, f, "pathpkg", "path")
+// adds
+//	import pathpkg "path"
+//
+// AddNamedImport only returns edits that affect the import declarations.
+func AddNamedImport(fset *token.FileSet, f *ast.File, name, path string) (edits []TextEdit, err error) {
+	if alreadyImports(f, name, path) {
+		return nil, nil
+	}
+
+	newImport := &ast.ImportSpec{
+		Path: &ast.BasicLit{
+			Kind:  token.STRING,
+			Value: strconv.Quote(path),
+		},
+	}
+	if name != "" {
+		newImport.Name = &ast.Ident{Name: name}
+	}
+
+	// TODO(suzmue): insert the import statement in the location that would be chosen
+	// by astutil.AddNamedImport
+	// Find the last import decl.
+	var lastImport = -1 // index in f.Decls of the file's final import decl
+	for i, decl := range f.Decls {
+		gen, ok := decl.(*ast.GenDecl)
+		if ok && gen.Tok == token.IMPORT {
+			lastImport = i
+		}
+	}
+
+	// Add an import decl after the last import.
+	impDecl := &ast.GenDecl{
+		Tok: token.IMPORT,
+	}
+	impDecl.Specs = append(impDecl.Specs, newImport)
+
+	var insertPos token.Pos
+	if lastImport >= 0 {
+		insertPos = f.Decls[lastImport].End()
+	} else {
+		// There are no existing imports.
+		// Our new import, preceded by a blank line,  goes after the package declaration
+		// and after the comment, if any, that starts on the same line as the
+		// package declaration.
+		insertPos = f.Name.End()
+
+		file := fset.File(f.Package)
+		pkgLine := file.Line(f.Package)
+		for _, c := range f.Comments {
+			if file.Line(c.Pos()) > pkgLine {
+				break
+			}
+			insertPos = c.End()
+		}
+	}
+
+	// Print this import declaration.
+	var buf bytes.Buffer
+	format.Node(&buf, fset, impDecl)
+	newText := "\n\n" + buf.String() + "\n"
+
+	rng := span.NewRange(fset, insertPos, insertPos)
+	spn, err := rng.Span()
+	if err != nil {
+		return nil, err
+	}
+
+	edits = append(edits, TextEdit{
+		Span:    spn,
+		NewText: newText,
+	})
+	return edits, nil
+}
+
+// alreadyImports reports whether f has an import with the specified name and path.
+func alreadyImports(f *ast.File, name, path string) bool {
+	for _, s := range f.Imports {
+		if importName(s) == name && importPath(s) == path {
+			return true
+		}
+	}
+	return false
+}
+
+// importName returns the name of s,
+// or "" if the import is not named.
+func importName(s *ast.ImportSpec) string {
+	if s.Name == nil {
+		return ""
+	}
+	return s.Name.Name
+}
+
+// importPath returns the unquoted import path of s,
+// or "" if the path is not properly quoted.
+func importPath(s *ast.ImportSpec) string {
+	t, err := strconv.Unquote(s.Path.Value)
+	if err != nil {
+		return ""
+	}
+	return t
+}
diff --git a/internal/lsp/source/imports_test.go b/internal/lsp/source/imports_test.go
new file mode 100644
index 0000000..76c86ae
--- /dev/null
+++ b/internal/lsp/source/imports_test.go
@@ -0,0 +1,306 @@
+package source
+
+import (
+	"bytes"
+	"fmt"
+	"go/ast"
+	"go/format"
+	"go/parser"
+	"go/token"
+	"log"
+	"testing"
+)
+
+var fset = token.NewFileSet()
+
+func parse(t *testing.T, name, in string) *ast.File {
+	file, err := parser.ParseFile(fset, name, in, parser.ParseComments)
+	if err != nil {
+		t.Fatalf("%s parse: %v", name, err)
+	}
+	return file
+}
+
+func print(t *testing.T, name string, f *ast.File) string {
+	var buf bytes.Buffer
+	if err := format.Node(&buf, fset, f); err != nil {
+		t.Fatalf("%s gofmt: %v", name, err)
+	}
+	return buf.String()
+}
+
+type test struct {
+	name       string
+	renamedPkg string
+	pkg        string
+	in         string
+	want       []importInfo
+	unchanged  bool // Expect added/deleted return value to be false.
+}
+
+type importInfo struct {
+	name string
+	path string
+}
+
+var addTests = []test{
+	{
+		name: "leave os alone",
+		pkg:  "os",
+		in: `package main
+
+import (
+	"os"
+)
+`,
+		want: []importInfo{
+			importInfo{
+				name: "",
+				path: "os",
+			},
+		},
+		unchanged: true,
+	},
+	{
+		name: "package statement only",
+		pkg:  "os",
+		in: `package main
+`,
+		want: []importInfo{
+			importInfo{
+				name: "",
+				path: "os",
+			},
+		},
+	},
+	{
+		name: "package statement no new line",
+		pkg:  "os",
+		in:   `package main`,
+		want: []importInfo{
+			importInfo{
+				name: "",
+				path: "os",
+			},
+		},
+	},
+	{
+		name: "package statement comments",
+		pkg:  "os",
+		in: `// This is a comment
+package main // This too`,
+		want: []importInfo{
+			importInfo{
+				name: "",
+				path: "os",
+			},
+		},
+	},
+	{
+		name: "package statement multiline comments",
+		pkg:  "os",
+		in: `package main /* This is a multiline comment
+and it extends 
+further down*/`,
+		want: []importInfo{
+			importInfo{
+				name: "",
+				path: "os",
+			},
+		},
+	},
+	{
+		name: "import c",
+		pkg:  "os",
+		in: `package main 
+
+import "C"
+`,
+		want: []importInfo{
+			importInfo{
+				name: "",
+				path: "os",
+			},
+			importInfo{
+				name: "",
+				path: "C",
+			},
+		},
+	},
+	{
+		name: "existing imports",
+		pkg:  "os",
+		in: `package main 
+
+import "io"
+`,
+		want: []importInfo{
+			importInfo{
+				name: "",
+				path: "os",
+			},
+			importInfo{
+				name: "",
+				path: "io",
+			},
+		},
+	},
+	{
+		name: "existing imports with comment",
+		pkg:  "os",
+		in: `package main 
+
+import "io" // A comment
+`,
+		want: []importInfo{
+			importInfo{
+				name: "",
+				path: "os",
+			},
+			importInfo{
+				name: "",
+				path: "io",
+			},
+		},
+	},
+	{
+		name: "existing imports multiline comment",
+		pkg:  "os",
+		in: `package main 
+
+import "io" /* A comment
+that
+extends */
+`,
+		want: []importInfo{
+			importInfo{
+				name: "",
+				path: "os",
+			},
+			importInfo{
+				name: "",
+				path: "io",
+			},
+		},
+	},
+	{
+		name:       "renamed import",
+		renamedPkg: "o",
+		pkg:        "os",
+		in: `package main 
+`,
+		want: []importInfo{
+			importInfo{
+				name: "o",
+				path: "os",
+			},
+		},
+	},
+}
+
+func TestAddImport(t *testing.T) {
+	for _, test := range addTests {
+		file := parse(t, test.name, test.in)
+		var before bytes.Buffer
+		ast.Fprint(&before, fset, file, nil)
+		edits, err := AddNamedImport(fset, file, test.renamedPkg, test.pkg)
+		if err != nil && !test.unchanged {
+			t.Errorf("error adding import: %s", err)
+			continue
+		}
+
+		// Apply the edits and parse the file.
+		got := applyEdits(test.in, edits)
+		gotFile := parse(t, test.name, got)
+
+		compareImports(t, fmt.Sprintf("first run: %s:\n", test.name), gotFile.Imports, test.want)
+
+		// AddNamedImport should be idempotent. Verify that by calling it again,
+		// expecting no change to the AST, and the returned added value to always be false.
+		edits, err = AddNamedImport(fset, gotFile, test.renamedPkg, test.pkg)
+		if err != nil && !test.unchanged {
+			t.Errorf("error adding import: %s", err)
+			continue
+		}
+		// Apply the edits and parse the file.
+		got = applyEdits(got, edits)
+		gotFile = parse(t, test.name, got)
+
+		compareImports(t, test.name, gotFile.Imports, test.want)
+
+	}
+}
+
+func TestDoubleAddNamedImport(t *testing.T) {
+	name := "doublenamedimport"
+	in := "package main\n"
+	file := parse(t, name, in)
+	// Add a named import
+	edits, err := AddNamedImport(fset, file, "o", "os")
+	if err != nil {
+		t.Errorf("error adding import: %s", err)
+		return
+	}
+	got := applyEdits(in, edits)
+	log.Println(got)
+	gotFile := parse(t, name, got)
+
+	// Add a second named import
+	edits, err = AddNamedImport(fset, gotFile, "i", "io")
+	if err != nil {
+		t.Errorf("error adding import: %s", err)
+		return
+	}
+	got = applyEdits(got, edits)
+	gotFile = parse(t, name, got)
+
+	want := []importInfo{
+		importInfo{
+			name: "o",
+			path: "os",
+		},
+		importInfo{
+			name: "i",
+			path: "io",
+		},
+	}
+	compareImports(t, "", gotFile.Imports, want)
+}
+
+func compareImports(t *testing.T, prefix string, got []*ast.ImportSpec, want []importInfo) {
+	if len(got) != len(want) {
+		t.Errorf("%s\ngot %d imports\nwant %d", prefix, len(got), len(want))
+		return
+	}
+
+	for _, imp := range got {
+		name := importName(imp)
+		path := importPath(imp)
+		found := false
+		for _, want := range want {
+			if want.name == name && want.path == path {
+				found = true
+				break
+			}
+		}
+		if !found {
+			t.Errorf("%s\n\ngot unexpected import: name: %q,path: %q", prefix, name, path)
+			continue
+		}
+	}
+}
+
+func applyEdits(contents string, edits []TextEdit) string {
+	res := contents
+
+	// Apply the edits from the end of the file forward
+	// to preserve the offsets
+	for i := len(edits) - 1; i >= 0; i-- {
+		edit := edits[i]
+		start := edit.Span.Start().Offset()
+		end := edit.Span.End().Offset()
+		tmp := res[0:start] + edit.NewText
+		res = tmp + res[end:]
+	}
+	return res
+}
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index a53ed7e..16d92f2 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -93,9 +93,11 @@
 		}
 		pos := tok.Pos(src.Start().Offset())
 		deepComplete := strings.Contains(string(src.URI()), "deepcomplete")
+		unimported := strings.Contains(string(src.URI()), "unimported")
 		list, surrounding, err := source.Completion(ctx, r.view, f.(source.GoFile), pos, source.CompletionOptions{
 			DeepComplete:     deepComplete,
 			WantDocumentaton: true,
+			WantUnimported:   unimported,
 		})
 		if err != nil {
 			t.Fatalf("failed for %v: %v", src, err)
diff --git a/internal/lsp/testdata/unimported/mkunimported.go b/internal/lsp/testdata/unimported/mkunimported.go
new file mode 100644
index 0000000..0656aba
--- /dev/null
+++ b/internal/lsp/testdata/unimported/mkunimported.go
@@ -0,0 +1,115 @@
+// +build ignore
+
+// mkunimported generates the unimported.go file, containing the Go standard
+// library packages.
+// The completion items from the std library are computed in the same way as in the
+// golang.org/x/tools/internal/imports.
+package main
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"go/format"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	pkgpath "path"
+	"path/filepath"
+	"regexp"
+	"runtime"
+	"sort"
+	"strings"
+)
+
+func mustOpen(name string) io.Reader {
+	f, err := os.Open(name)
+	if err != nil {
+		log.Fatal(err)
+	}
+	return f
+}
+
+func api(base string) string {
+	return filepath.Join(runtime.GOROOT(), "api", base)
+}
+
+var sym = regexp.MustCompile(`^pkg (\S+).*?, (?:var|func|type|const) ([A-Z]\w*)`)
+
+var unsafeSyms = map[string]bool{"Alignof": true, "ArbitraryType": true, "Offsetof": true, "Pointer": true, "Sizeof": true}
+
+func main() {
+	var buf bytes.Buffer
+	outf := func(format string, args ...interface{}) {
+		fmt.Fprintf(&buf, format, args...)
+	}
+	outf("// Code generated by mkstdlib.go. DO NOT EDIT.\n\n")
+	outf("package unimported\n")
+	outf("func _() {\n")
+	f := io.MultiReader(
+		mustOpen(api("go1.txt")),
+		mustOpen(api("go1.1.txt")),
+		mustOpen(api("go1.2.txt")),
+		mustOpen(api("go1.3.txt")),
+		mustOpen(api("go1.4.txt")),
+		mustOpen(api("go1.5.txt")),
+		mustOpen(api("go1.6.txt")),
+		mustOpen(api("go1.7.txt")),
+		mustOpen(api("go1.8.txt")),
+		mustOpen(api("go1.9.txt")),
+		mustOpen(api("go1.10.txt")),
+		mustOpen(api("go1.11.txt")),
+		mustOpen(api("go1.12.txt")),
+	)
+	sc := bufio.NewScanner(f)
+
+	pkgs := map[string]bool{
+		"unsafe":     true,
+		"syscall/js": true,
+	}
+	paths := []string{"unsafe", "syscall/js"}
+	for sc.Scan() {
+		l := sc.Text()
+		has := func(v string) bool { return strings.Contains(l, v) }
+		if has("struct, ") || has("interface, ") || has(", method (") {
+			continue
+		}
+		if m := sym.FindStringSubmatch(l); m != nil {
+			path, _ := m[1], m[2]
+
+			if _, ok := pkgs[path]; !ok {
+				pkgs[path] = true
+				paths = append(paths, path)
+			}
+		}
+	}
+	if err := sc.Err(); err != nil {
+		log.Fatal(err)
+	}
+	sort.Strings(paths)
+
+	var markers []string
+	for _, path := range paths {
+		marker := strings.ReplaceAll(path, "/", "slash")
+		markers = append(markers, marker)
+	}
+	outf("	//@complete(\"\", %s)\n", strings.Join(markers, ", "))
+	outf("}\n")
+	outf("// Create markers for unimported std lib packages. Only for use by this test.\n")
+
+	for i, path := range paths {
+		name := pkgpath.Base(path)
+		marker := markers[i]
+		outf("/* %s *///@item(%s, \"%s\", \"\\\"%s\\\"\", \"package\")\n", name, marker, name, path)
+	}
+
+	fmtbuf, err := format.Source(buf.Bytes())
+	if err != nil {
+		log.Fatal(err)
+	}
+	err = ioutil.WriteFile("unimported.go", fmtbuf, 0666)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
diff --git a/internal/lsp/testdata/unimported/unimported.go b/internal/lsp/testdata/unimported/unimported.go
new file mode 100644
index 0000000..be3a4ce
--- /dev/null
+++ b/internal/lsp/testdata/unimported/unimported.go
@@ -0,0 +1,151 @@
+// Code generated by mkstdlib.go. DO NOT EDIT.
+
+package unimported
+
+func _() {
+	//@complete("", archiveslashtar, archiveslashzip, bufio, bytes, compressslashbzip2, compressslashflate, compressslashgzip, compressslashlzw, compressslashzlib, containerslashheap, containerslashlist, containerslashring, context, crypto, cryptoslashaes, cryptoslashcipher, cryptoslashdes, cryptoslashdsa, cryptoslashecdsa, cryptoslashelliptic, cryptoslashhmac, cryptoslashmd5, cryptoslashrand, cryptoslashrc4, cryptoslashrsa, cryptoslashsha1, cryptoslashsha256, cryptoslashsha512, cryptoslashsubtle, cryptoslashtls, cryptoslashx509, cryptoslashx509slashpkix, databaseslashsql, databaseslashsqlslashdriver, debugslashdwarf, debugslashelf, debugslashgosym, debugslashmacho, debugslashpe, debugslashplan9obj, encoding, encodingslashascii85, encodingslashasn1, encodingslashbase32, encodingslashbase64, encodingslashbinary, encodingslashcsv, encodingslashgob, encodingslashhex, encodingslashjson, encodingslashpem, encodingslashxml, errors, expvar, flag, fmt, goslashast, goslashbuild, goslashconstant, goslashdoc, goslashformat, goslashimporter, goslashparser, goslashprinter, goslashscanner, goslashtoken, goslashtypes, hash, hashslashadler32, hashslashcrc32, hashslashcrc64, hashslashfnv, html, htmlslashtemplate, image, imageslashcolor, imageslashcolorslashpalette, imageslashdraw, imageslashgif, imageslashjpeg, imageslashpng, indexslashsuffixarray, io, ioslashioutil, log, logslashsyslog, math, mathslashbig, mathslashbits, mathslashcmplx, mathslashrand, mime, mimeslashmultipart, mimeslashquotedprintable, net, netslashhttp, netslashhttpslashcgi, netslashhttpslashcookiejar, netslashhttpslashfcgi, netslashhttpslashhttptest, netslashhttpslashhttptrace, netslashhttpslashhttputil, netslashhttpslashpprof, netslashmail, netslashrpc, netslashrpcslashjsonrpc, netslashsmtp, netslashtextproto, netslashurl, os, osslashexec, osslashsignal, osslashuser, path, pathslashfilepath, plugin, reflect, regexp, regexpslashsyntax, runtime, runtimeslashdebug, runtimeslashpprof, runtimeslashtrace, sort, strconv, strings, sync, syncslashatomic, syscall, syscallslashjs, testing, testingslashiotest, testingslashquick, textslashscanner, textslashtabwriter, textslashtemplate, textslashtemplateslashparse, time, unicode, unicodeslashutf16, unicodeslashutf8, unsafe)
+}
+
+// Create markers for unimported std lib packages. Only for use by this test.
+/* tar */ //@item(archiveslashtar, "tar", "\"archive/tar\"", "package")
+/* zip */ //@item(archiveslashzip, "zip", "\"archive/zip\"", "package")
+/* bufio */ //@item(bufio, "bufio", "\"bufio\"", "package")
+/* bytes */ //@item(bytes, "bytes", "\"bytes\"", "package")
+/* bzip2 */ //@item(compressslashbzip2, "bzip2", "\"compress/bzip2\"", "package")
+/* flate */ //@item(compressslashflate, "flate", "\"compress/flate\"", "package")
+/* gzip */ //@item(compressslashgzip, "gzip", "\"compress/gzip\"", "package")
+/* lzw */ //@item(compressslashlzw, "lzw", "\"compress/lzw\"", "package")
+/* zlib */ //@item(compressslashzlib, "zlib", "\"compress/zlib\"", "package")
+/* heap */ //@item(containerslashheap, "heap", "\"container/heap\"", "package")
+/* list */ //@item(containerslashlist, "list", "\"container/list\"", "package")
+/* ring */ //@item(containerslashring, "ring", "\"container/ring\"", "package")
+/* context */ //@item(context, "context", "\"context\"", "package")
+/* crypto */ //@item(crypto, "crypto", "\"crypto\"", "package")
+/* aes */ //@item(cryptoslashaes, "aes", "\"crypto/aes\"", "package")
+/* cipher */ //@item(cryptoslashcipher, "cipher", "\"crypto/cipher\"", "package")
+/* des */ //@item(cryptoslashdes, "des", "\"crypto/des\"", "package")
+/* dsa */ //@item(cryptoslashdsa, "dsa", "\"crypto/dsa\"", "package")
+/* ecdsa */ //@item(cryptoslashecdsa, "ecdsa", "\"crypto/ecdsa\"", "package")
+/* elliptic */ //@item(cryptoslashelliptic, "elliptic", "\"crypto/elliptic\"", "package")
+/* hmac */ //@item(cryptoslashhmac, "hmac", "\"crypto/hmac\"", "package")
+/* md5 */ //@item(cryptoslashmd5, "md5", "\"crypto/md5\"", "package")
+/* rand */ //@item(cryptoslashrand, "rand", "\"crypto/rand\"", "package")
+/* rc4 */ //@item(cryptoslashrc4, "rc4", "\"crypto/rc4\"", "package")
+/* rsa */ //@item(cryptoslashrsa, "rsa", "\"crypto/rsa\"", "package")
+/* sha1 */ //@item(cryptoslashsha1, "sha1", "\"crypto/sha1\"", "package")
+/* sha256 */ //@item(cryptoslashsha256, "sha256", "\"crypto/sha256\"", "package")
+/* sha512 */ //@item(cryptoslashsha512, "sha512", "\"crypto/sha512\"", "package")
+/* subtle */ //@item(cryptoslashsubtle, "subtle", "\"crypto/subtle\"", "package")
+/* tls */ //@item(cryptoslashtls, "tls", "\"crypto/tls\"", "package")
+/* x509 */ //@item(cryptoslashx509, "x509", "\"crypto/x509\"", "package")
+/* pkix */ //@item(cryptoslashx509slashpkix, "pkix", "\"crypto/x509/pkix\"", "package")
+/* sql */ //@item(databaseslashsql, "sql", "\"database/sql\"", "package")
+/* driver */ //@item(databaseslashsqlslashdriver, "driver", "\"database/sql/driver\"", "package")
+/* dwarf */ //@item(debugslashdwarf, "dwarf", "\"debug/dwarf\"", "package")
+/* elf */ //@item(debugslashelf, "elf", "\"debug/elf\"", "package")
+/* gosym */ //@item(debugslashgosym, "gosym", "\"debug/gosym\"", "package")
+/* macho */ //@item(debugslashmacho, "macho", "\"debug/macho\"", "package")
+/* pe */ //@item(debugslashpe, "pe", "\"debug/pe\"", "package")
+/* plan9obj */ //@item(debugslashplan9obj, "plan9obj", "\"debug/plan9obj\"", "package")
+/* encoding */ //@item(encoding, "encoding", "\"encoding\"", "package")
+/* ascii85 */ //@item(encodingslashascii85, "ascii85", "\"encoding/ascii85\"", "package")
+/* asn1 */ //@item(encodingslashasn1, "asn1", "\"encoding/asn1\"", "package")
+/* base32 */ //@item(encodingslashbase32, "base32", "\"encoding/base32\"", "package")
+/* base64 */ //@item(encodingslashbase64, "base64", "\"encoding/base64\"", "package")
+/* binary */ //@item(encodingslashbinary, "binary", "\"encoding/binary\"", "package")
+/* csv */ //@item(encodingslashcsv, "csv", "\"encoding/csv\"", "package")
+/* gob */ //@item(encodingslashgob, "gob", "\"encoding/gob\"", "package")
+/* hex */ //@item(encodingslashhex, "hex", "\"encoding/hex\"", "package")
+/* json */ //@item(encodingslashjson, "json", "\"encoding/json\"", "package")
+/* pem */ //@item(encodingslashpem, "pem", "\"encoding/pem\"", "package")
+/* xml */ //@item(encodingslashxml, "xml", "\"encoding/xml\"", "package")
+/* errors */ //@item(errors, "errors", "\"errors\"", "package")
+/* expvar */ //@item(expvar, "expvar", "\"expvar\"", "package")
+/* flag */ //@item(flag, "flag", "\"flag\"", "package")
+/* fmt */ //@item(fmt, "fmt", "\"fmt\"", "package")
+/* ast */ //@item(goslashast, "ast", "\"go/ast\"", "package")
+/* build */ //@item(goslashbuild, "build", "\"go/build\"", "package")
+/* constant */ //@item(goslashconstant, "constant", "\"go/constant\"", "package")
+/* doc */ //@item(goslashdoc, "doc", "\"go/doc\"", "package")
+/* format */ //@item(goslashformat, "format", "\"go/format\"", "package")
+/* importer */ //@item(goslashimporter, "importer", "\"go/importer\"", "package")
+/* parser */ //@item(goslashparser, "parser", "\"go/parser\"", "package")
+/* printer */ //@item(goslashprinter, "printer", "\"go/printer\"", "package")
+/* scanner */ //@item(goslashscanner, "scanner", "\"go/scanner\"", "package")
+/* token */ //@item(goslashtoken, "token", "\"go/token\"", "package")
+/* types */ //@item(goslashtypes, "types", "\"go/types\"", "package")
+/* hash */ //@item(hash, "hash", "\"hash\"", "package")
+/* adler32 */ //@item(hashslashadler32, "adler32", "\"hash/adler32\"", "package")
+/* crc32 */ //@item(hashslashcrc32, "crc32", "\"hash/crc32\"", "package")
+/* crc64 */ //@item(hashslashcrc64, "crc64", "\"hash/crc64\"", "package")
+/* fnv */ //@item(hashslashfnv, "fnv", "\"hash/fnv\"", "package")
+/* html */ //@item(html, "html", "\"html\"", "package")
+/* template */ //@item(htmlslashtemplate, "template", "\"html/template\"", "package")
+/* image */ //@item(image, "image", "\"image\"", "package")
+/* color */ //@item(imageslashcolor, "color", "\"image/color\"", "package")
+/* palette */ //@item(imageslashcolorslashpalette, "palette", "\"image/color/palette\"", "package")
+/* draw */ //@item(imageslashdraw, "draw", "\"image/draw\"", "package")
+/* gif */ //@item(imageslashgif, "gif", "\"image/gif\"", "package")
+/* jpeg */ //@item(imageslashjpeg, "jpeg", "\"image/jpeg\"", "package")
+/* png */ //@item(imageslashpng, "png", "\"image/png\"", "package")
+/* suffixarray */ //@item(indexslashsuffixarray, "suffixarray", "\"index/suffixarray\"", "package")
+/* io */ //@item(io, "io", "\"io\"", "package")
+/* ioutil */ //@item(ioslashioutil, "ioutil", "\"io/ioutil\"", "package")
+/* log */ //@item(log, "log", "\"log\"", "package")
+/* syslog */ //@item(logslashsyslog, "syslog", "\"log/syslog\"", "package")
+/* math */ //@item(math, "math", "\"math\"", "package")
+/* big */ //@item(mathslashbig, "big", "\"math/big\"", "package")
+/* bits */ //@item(mathslashbits, "bits", "\"math/bits\"", "package")
+/* cmplx */ //@item(mathslashcmplx, "cmplx", "\"math/cmplx\"", "package")
+/* rand */ //@item(mathslashrand, "rand", "\"math/rand\"", "package")
+/* mime */ //@item(mime, "mime", "\"mime\"", "package")
+/* multipart */ //@item(mimeslashmultipart, "multipart", "\"mime/multipart\"", "package")
+/* quotedprintable */ //@item(mimeslashquotedprintable, "quotedprintable", "\"mime/quotedprintable\"", "package")
+/* net */ //@item(net, "net", "\"net\"", "package")
+/* http */ //@item(netslashhttp, "http", "\"net/http\"", "package")
+/* cgi */ //@item(netslashhttpslashcgi, "cgi", "\"net/http/cgi\"", "package")
+/* cookiejar */ //@item(netslashhttpslashcookiejar, "cookiejar", "\"net/http/cookiejar\"", "package")
+/* fcgi */ //@item(netslashhttpslashfcgi, "fcgi", "\"net/http/fcgi\"", "package")
+/* httptest */ //@item(netslashhttpslashhttptest, "httptest", "\"net/http/httptest\"", "package")
+/* httptrace */ //@item(netslashhttpslashhttptrace, "httptrace", "\"net/http/httptrace\"", "package")
+/* httputil */ //@item(netslashhttpslashhttputil, "httputil", "\"net/http/httputil\"", "package")
+/* pprof */ //@item(netslashhttpslashpprof, "pprof", "\"net/http/pprof\"", "package")
+/* mail */ //@item(netslashmail, "mail", "\"net/mail\"", "package")
+/* rpc */ //@item(netslashrpc, "rpc", "\"net/rpc\"", "package")
+/* jsonrpc */ //@item(netslashrpcslashjsonrpc, "jsonrpc", "\"net/rpc/jsonrpc\"", "package")
+/* smtp */ //@item(netslashsmtp, "smtp", "\"net/smtp\"", "package")
+/* textproto */ //@item(netslashtextproto, "textproto", "\"net/textproto\"", "package")
+/* url */ //@item(netslashurl, "url", "\"net/url\"", "package")
+/* os */ //@item(os, "os", "\"os\"", "package")
+/* exec */ //@item(osslashexec, "exec", "\"os/exec\"", "package")
+/* signal */ //@item(osslashsignal, "signal", "\"os/signal\"", "package")
+/* user */ //@item(osslashuser, "user", "\"os/user\"", "package")
+/* path */ //@item(path, "path", "\"path\"", "package")
+/* filepath */ //@item(pathslashfilepath, "filepath", "\"path/filepath\"", "package")
+/* plugin */ //@item(plugin, "plugin", "\"plugin\"", "package")
+/* reflect */ //@item(reflect, "reflect", "\"reflect\"", "package")
+/* regexp */ //@item(regexp, "regexp", "\"regexp\"", "package")
+/* syntax */ //@item(regexpslashsyntax, "syntax", "\"regexp/syntax\"", "package")
+/* runtime */ //@item(runtime, "runtime", "\"runtime\"", "package")
+/* debug */ //@item(runtimeslashdebug, "debug", "\"runtime/debug\"", "package")
+/* pprof */ //@item(runtimeslashpprof, "pprof", "\"runtime/pprof\"", "package")
+/* trace */ //@item(runtimeslashtrace, "trace", "\"runtime/trace\"", "package")
+/* sort */ //@item(sort, "sort", "\"sort\"", "package")
+/* strconv */ //@item(strconv, "strconv", "\"strconv\"", "package")
+/* strings */ //@item(strings, "strings", "\"strings\"", "package")
+/* sync */ //@item(sync, "sync", "\"sync\"", "package")
+/* atomic */ //@item(syncslashatomic, "atomic", "\"sync/atomic\"", "package")
+/* syscall */ //@item(syscall, "syscall", "\"syscall\"", "package")
+/* js */ //@item(syscallslashjs, "js", "\"syscall/js\"", "package")
+/* testing */ //@item(testing, "testing", "\"testing\"", "package")
+/* iotest */ //@item(testingslashiotest, "iotest", "\"testing/iotest\"", "package")
+/* quick */ //@item(testingslashquick, "quick", "\"testing/quick\"", "package")
+/* scanner */ //@item(textslashscanner, "scanner", "\"text/scanner\"", "package")
+/* tabwriter */ //@item(textslashtabwriter, "tabwriter", "\"text/tabwriter\"", "package")
+/* template */ //@item(textslashtemplate, "template", "\"text/template\"", "package")
+/* parse */ //@item(textslashtemplateslashparse, "parse", "\"text/template/parse\"", "package")
+/* time */ //@item(time, "time", "\"time\"", "package")
+/* unicode */ //@item(unicode, "unicode", "\"unicode\"", "package")
+/* utf16 */ //@item(unicodeslashutf16, "utf16", "\"unicode/utf16\"", "package")
+/* utf8 */ //@item(unicodeslashutf8, "utf8", "\"unicode/utf8\"", "package")
+/* unsafe */ //@item(unsafe, "unsafe", "\"unsafe\"", "package")
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index 2233ad9..e83c695 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -29,7 +29,7 @@
 // We hardcode the expected number of test cases to ensure that all tests
 // are being executed. If a test is added, this number must be changed.
 const (
-	ExpectedCompletionsCount       = 145
+	ExpectedCompletionsCount       = 146
 	ExpectedCompletionSnippetCount = 15
 	ExpectedDiagnosticsCount       = 21
 	ExpectedFormatCount            = 6