internal/lsp/cache: automatically construct the workspace module

This change adds an experimental configuration, which when enabled,
shifts gopls to operate in multi-module mode. It implements the
super-module as described in
https://github.com/golang/proposal/blob/master/design/37720-gopls-workspaces.md.
Replace directives are also added when a workspace module requires
another workspace module (which has not yet been mentioned in the design
doc).

A user-provided workspace gopls.mod file is not yet supported, as it is
not yet testable. Clients will need to add support for change
notifications for the gopls.mod once it is added.

Updates golang/go#32394

Change-Id: I5089358603bca34c5c8db9e5a00f93e1cca0b93f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/247819
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md
index c8b1299..2524861 100644
--- a/gopls/doc/settings.md
+++ b/gopls/doc/settings.md
@@ -205,4 +205,9 @@
 your entire module. This is particularly useful for those working in a monorepo.
 
 Default: `true`.
+### **experimentalWorkspaceModule** *bool*
+experimentalWorkspaceModule opts a user into the experimental support
+for multi-module workspaces.
+
+Default: `false`.
 <!-- END Experimental: DO NOT MANUALLY EDIT THIS SECTION -->
diff --git a/gopls/internal/regtest/env.go b/gopls/internal/regtest/env.go
index 262851f..6bcfd5c 100644
--- a/gopls/internal/regtest/env.go
+++ b/gopls/internal/regtest/env.go
@@ -700,6 +700,39 @@
 	}
 }
 
+// NoDiagnosticAtRegexp expects that there is no diagnostic entry at the start
+// position matching the regexp search string re in the buffer specified by
+// name. Note that this currently ignores the end position.
+// This should only be used in combination with OnceMet for a given condition,
+// otherwise it may always succeed.
+func (e *Env) NoDiagnosticAtRegexp(name, re string) DiagnosticExpectation {
+	e.T.Helper()
+	pos := e.RegexpSearch(name, re)
+	expectation := NoDiagnosticAt(name, pos.Line, pos.Column)
+	expectation.description += fmt.Sprintf(" (location of %q)", re)
+	return expectation
+}
+
+// NoDiagnosticAt asserts that there is no diagnostic entry at the position
+// specified by line and col, for the workdir-relative path name.
+// This should only be used in combination with OnceMet for a given condition,
+// otherwise it may always succeed.
+func NoDiagnosticAt(name string, line, col int) DiagnosticExpectation {
+	isMet := func(diags *protocol.PublishDiagnosticsParams) bool {
+		for _, d := range diags.Diagnostics {
+			if d.Range.Start.Line == float64(line) && d.Range.Start.Character == float64(col) {
+				return false
+			}
+		}
+		return true
+	}
+	return DiagnosticExpectation{
+		isMet:       isMet,
+		description: fmt.Sprintf("no diagnostic at {line:%d, column:%d}", line, col),
+		path:        name,
+	}
+}
+
 // DiagnosticsFor returns the current diagnostics for the file. It is useful
 // after waiting on AnyDiagnosticAtCurrentVersion, when the desired diagnostic
 // is not simply described by DiagnosticAt.
diff --git a/gopls/internal/regtest/workspace_test.go b/gopls/internal/regtest/workspace_test.go
index 4bb31e3..7b997ae 100644
--- a/gopls/internal/regtest/workspace_test.go
+++ b/gopls/internal/regtest/workspace_test.go
@@ -9,6 +9,7 @@
 	"testing"
 
 	"golang.org/x/tools/internal/lsp"
+	"golang.org/x/tools/internal/lsp/fake"
 )
 
 const workspaceProxy = `
@@ -156,3 +157,57 @@
 		)
 	})
 }
+
+const workspaceModuleProxy = `
+-- b.com@v1.2.3/go.mod --
+module b.com
+
+go 1.12
+-- b.com@v1.2.3/b/b.go --
+package b
+
+func Hello() {}
+`
+
+func TestAutomaticWorkspaceModule_Interdependent(t *testing.T) {
+	const multiModule = `
+-- moda/a/go.mod --
+module a.com
+
+require b.com v1.2.3
+
+-- moda/a/a.go --
+package a
+
+import (
+	"b.com/b"
+)
+
+func main() {
+	var x int
+	_ = b.Hello()
+}
+-- modb/go.mod --
+module b.com
+
+-- modb/b/b.go --
+package b
+
+func Hello() int {
+	var x int
+}
+`
+	withOptions(
+		WithProxyFiles(workspaceModuleProxy),
+		WithEditorConfig(fake.EditorConfig{ExperimentalWorkspaceModule: true}),
+	).run(t, multiModule, func(t *testing.T, env *Env) {
+		env.Await(
+			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1),
+		)
+		env.Await(
+			env.DiagnosticAtRegexp("moda/a/a.go", "x"),
+			env.DiagnosticAtRegexp("modb/b/b.go", "x"),
+			env.NoDiagnosticAtRegexp("moda/a/a.go", `"b.com/b"`),
+		)
+	})
+}
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index 4b93bba..52ff7ee 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -8,6 +8,9 @@
 	"context"
 	"fmt"
 	"go/types"
+	"io/ioutil"
+	"os"
+	"path/filepath"
 	"sort"
 	"strings"
 
@@ -62,6 +65,8 @@
 				q = "./..."
 			}
 			query = append(query, q)
+		case moduleLoadScope:
+			query = append(query, fmt.Sprintf("%s/...", scope))
 		case viewLoadScope:
 			// If we are outside of GOPATH, a module, or some other known
 			// build system, don't load subdirectories.
@@ -84,8 +89,20 @@
 	defer done()
 
 	cfg := s.config(ctx)
+
 	cleanup := func() {}
-	if s.view.tmpMod {
+	switch {
+	case s.view.workspaceMode&workspaceModule != 0:
+		var (
+			tmpDir span.URI
+			err    error
+		)
+		tmpDir, cleanup, err = s.tempWorkspaceModule(ctx)
+		if err != nil {
+			return err
+		}
+		cfg.Dir = tmpDir.Filename()
+	case s.view.workspaceMode&tempModfile != 0:
 		modFH, err := s.GetFile(ctx, s.view.modURI)
 		if err != nil {
 			return err
@@ -129,7 +146,6 @@
 		}
 		return errors.Errorf("%v: %w", err, source.PackagesLoadError)
 	}
-
 	for _, pkg := range pkgs {
 		if !containsDir || s.view.Options().VerboseOutput {
 			event.Log(ctx, "go/packages.Load", tag.Snapshot.Of(s.ID()), tag.PackagePath.Of(pkg.PkgPath), tag.Files.Of(pkg.CompiledGoFiles))
@@ -165,6 +181,37 @@
 	return nil
 }
 
+// tempWorkspaceModule creates a temporary directory for use with
+// packages.Loads that occur from within the workspace module.
+func (s *snapshot) tempWorkspaceModule(ctx context.Context) (_ span.URI, cleanup func(), err error) {
+	cleanup = func() {}
+	if len(s.view.modules) == 0 {
+		return "", cleanup, nil
+	}
+	if s.view.workspaceModule == nil {
+		return "", cleanup, nil
+	}
+	content, err := s.view.workspaceModule.Format()
+	if err != nil {
+		return "", cleanup, err
+	}
+	// Create a temporary working directory for the go command that contains
+	// the workspace module file.
+	name, err := ioutil.TempDir("", "gopls-mod")
+	if err != nil {
+		return "", cleanup, err
+	}
+	cleanup = func() {
+		os.RemoveAll(name)
+	}
+	filename := filepath.Join(name, "go.mod")
+	if err := ioutil.WriteFile(filename, content, 0644); err != nil {
+		cleanup()
+		return "", cleanup, err
+	}
+	return span.URIFromPath(filepath.Dir(filename)), cleanup, nil
+}
+
 func (s *snapshot) setMetadata(ctx context.Context, pkgPath packagePath, pkg *packages.Package, cfg *packages.Config, seen map[packageID]struct{}) (*metadata, error) {
 	id := packageID(pkg.ID)
 	if _, ok := seen[id]; ok {
diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go
index e62d7fd..c689e2c 100644
--- a/internal/lsp/cache/mod.go
+++ b/internal/lsp/cache/mod.go
@@ -196,6 +196,9 @@
 }
 
 func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string]string, error) {
+	if fh.Kind() != source.Mod {
+		return nil, fmt.Errorf("%s is not a go.mod file", fh.URI())
+	}
 	if err := s.awaitLoaded(ctx); err != nil {
 		return nil, err
 	}
@@ -285,6 +288,9 @@
 }
 
 func (s *snapshot) ModUpgrade(ctx context.Context, fh source.FileHandle) (map[string]string, error) {
+	if fh.Kind() != source.Mod {
+		return nil, fmt.Errorf("%s is not a go.mod file", fh.URI())
+	}
 	if err := s.awaitLoaded(ctx); err != nil {
 		return nil, err
 	}
@@ -318,7 +324,7 @@
 		// Run "go list -mod readonly -u -m all" to be able to see which deps can be
 		// upgraded without modifying mod file.
 		args := []string{"-u", "-m", "-json", "all"}
-		if !snapshot.view.tmpMod || containsVendor(fh.URI()) {
+		if s.view.workspaceMode&tempModfile == 0 || containsVendor(fh.URI()) {
 			// Use -mod=readonly if the module contains a vendor directory
 			// (see golang/go#38711).
 			args = append([]string{"-mod", "readonly"}, args...)
diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go
index e2f010d..646a90f 100644
--- a/internal/lsp/cache/mod_tidy.go
+++ b/internal/lsp/cache/mod_tidy.go
@@ -52,7 +52,10 @@
 }
 
 func (s *snapshot) ModTidy(ctx context.Context, fh source.FileHandle) (*source.TidiedModule, error) {
-	if !s.view.tmpMod {
+	if fh.Kind() != source.Mod {
+		return nil, fmt.Errorf("%s is not a go.mod file", fh.URI())
+	}
+	if s.view.workspaceMode&tempModfile == 0 {
 		return nil, source.ErrTmpModfileUnsupported
 	}
 	if handle := s.getModTidyHandle(fh.URI()); handle != nil {
@@ -75,7 +78,7 @@
 	cfg := s.configWithDir(ctx, filepath.Dir(fh.URI().Filename()))
 	key := modTidyKey{
 		sessionID:       s.view.session.id,
-		view:            s.view.root.Filename(),
+		view:            s.view.folder.Filename(),
 		imports:         importHash,
 		unsavedOverlays: overlayHash,
 		gomod:           fh.FileIdentity(),
@@ -103,7 +106,7 @@
 				err: err,
 			}
 		}
-		tmpURI, runner, inv, cleanup, err := snapshot.goCommandInvocation(ctx, true, "mod", []string{"tidy"})
+		tmpURI, runner, inv, cleanup, err := snapshot.goCommandInvocation(ctx, cfg, true, "mod", []string{"tidy"})
 		if err != nil {
 			return &modTidyData{err: err}
 		}
diff --git a/internal/lsp/cache/pkg.go b/internal/lsp/cache/pkg.go
index 66321fb..b29eb27 100644
--- a/internal/lsp/cache/pkg.go
+++ b/internal/lsp/cache/pkg.go
@@ -41,9 +41,10 @@
 
 // Declare explicit types for files and directories to distinguish between the two.
 type (
-	fileURI       span.URI
-	directoryURI  span.URI
-	viewLoadScope span.URI
+	fileURI         span.URI
+	directoryURI    span.URI
+	moduleLoadScope string
+	viewLoadScope   span.URI
 )
 
 func (p *pkg) ID() string {
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index a55c770..a0aa93f 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -7,6 +7,8 @@
 import (
 	"context"
 	"fmt"
+	"os"
+	"path/filepath"
 	"strconv"
 	"strings"
 	"sync"
@@ -171,6 +173,7 @@
 		name:               name,
 		folder:             folder,
 		root:               folder,
+		modules:            make(map[span.URI]*module),
 		filesByURI:         make(map[span.URI]*fileBase),
 		filesByBase:        make(map[string][]*fileBase),
 	}
@@ -196,11 +199,21 @@
 	if v.session.cache.options != nil {
 		v.session.cache.options(&v.options)
 	}
+
 	// Set the module-specific information.
 	if err := v.setBuildInformation(ctx, folder, options); err != nil {
 		return nil, nil, func() {}, err
 	}
 
+	// Find all of the modules in the workspace.
+	if err := v.findAndBuildWorkspaceModule(ctx, options); err != nil {
+		return nil, nil, func() {}, err
+	}
+
+	// Now that we have set all required fields,
+	// check if the view has a valid build configuration.
+	v.setBuildConfiguration()
+
 	// We have v.goEnv now.
 	v.processEnv = &imports.ProcessEnv{
 		GocmdRunner: s.gocmdRunner,
@@ -227,6 +240,66 @@
 	return v, v.snapshot, v.snapshot.generation.Acquire(ctx), nil
 }
 
+// findAndBuildWorkspaceModule walks the view's root folder, looking for go.mod
+// files. Any that are found are added to the view's set of modules, which are
+// then used to construct the workspace module.
+//
+// It assumes that the caller has not yet created the view, and therefore does
+// not lock any of the internal data structures before accessing them.
+func (v *View) findAndBuildWorkspaceModule(ctx context.Context, options source.Options) error {
+	// If the user is intentionally limiting their workspace scope, add their
+	// folder to the roots and return early.
+	if !options.ExpandWorkspaceToModule {
+		return nil
+	}
+	// The workspace module has been disabled by the user.
+	if !options.ExperimentalWorkspaceModule {
+		return nil
+	}
+
+	v.workspaceMode |= workspaceModule
+
+	// Walk the view's folder to find all modules in the view.
+	root := v.root.Filename()
+	if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		// For any path that is not the workspace folder, check if the path
+		// would be ignored by the go command. Vendor directories also do not
+		// contain workspace modules.
+		if info.IsDir() && path != root {
+			suffix := strings.TrimPrefix(path, root)
+			switch {
+			case checkIgnored(suffix),
+				strings.Contains(filepath.ToSlash(suffix), "/vendor/"):
+				return filepath.SkipDir
+			}
+		}
+		// We're only interested in go.mod files.
+		if filepath.Base(path) != "go.mod" {
+			return nil
+		}
+		// At this point, we definitely have a go.mod file in the workspace,
+		// so add it to the view.
+		modURI := span.URIFromPath(path)
+		rootURI := span.URIFromPath(filepath.Dir(path))
+		v.modules[rootURI] = &module{
+			rootURI: rootURI,
+			modURI:  modURI,
+			sumURI:  span.URIFromPath(sumFilename(modURI)),
+		}
+		return nil
+	}); err != nil {
+		return err
+	}
+	// If the user does not have a gopls.mod, we need to create one, based on
+	// modules we found in the user's workspace.
+	var err error
+	v.workspaceModule, err = v.snapshot.buildWorkspaceModule(ctx)
+	return err
+}
+
 // View returns the view by name.
 func (s *Session) View(name string) source.View {
 	s.viewMu.Lock()
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index ddb3626..b72b62c 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -19,6 +19,7 @@
 	"strings"
 	"sync"
 
+	"golang.org/x/mod/modfile"
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/event"
@@ -168,7 +169,8 @@
 }
 
 func (s *snapshot) RunGoCommandDirect(ctx context.Context, verb string, args []string) error {
-	_, runner, inv, cleanup, err := s.goCommandInvocation(ctx, false, verb, args)
+	cfg := s.config(ctx)
+	_, runner, inv, cleanup, err := s.goCommandInvocation(ctx, cfg, false, verb, args)
 	if err != nil {
 		return err
 	}
@@ -179,7 +181,8 @@
 }
 
 func (s *snapshot) RunGoCommand(ctx context.Context, verb string, args []string) (*bytes.Buffer, error) {
-	_, runner, inv, cleanup, err := s.goCommandInvocation(ctx, true, verb, args)
+	cfg := s.config(ctx)
+	_, runner, inv, cleanup, err := s.goCommandInvocation(ctx, cfg, true, verb, args)
 	if err != nil {
 		return nil, err
 	}
@@ -189,7 +192,8 @@
 }
 
 func (s *snapshot) RunGoCommandPiped(ctx context.Context, verb string, args []string, stdout, stderr io.Writer) error {
-	_, runner, inv, cleanup, err := s.goCommandInvocation(ctx, true, verb, args)
+	cfg := s.config(ctx)
+	_, runner, inv, cleanup, err := s.goCommandInvocation(ctx, cfg, true, verb, args)
 	if err != nil {
 		return err
 	}
@@ -198,10 +202,9 @@
 }
 
 // Assumes that modURI is only provided when the -modfile flag is enabled.
-func (s *snapshot) goCommandInvocation(ctx context.Context, allowTempModfile bool, verb string, args []string) (tmpURI span.URI, runner *gocommand.Runner, inv *gocommand.Invocation, cleanup func(), err error) {
+func (s *snapshot) goCommandInvocation(ctx context.Context, cfg *packages.Config, allowTempModfile bool, verb string, args []string) (tmpURI span.URI, runner *gocommand.Runner, inv *gocommand.Invocation, cleanup func(), err error) {
 	cleanup = func() {} // fallback
-	cfg := s.config(ctx)
-	if allowTempModfile && s.view.tmpMod {
+	if allowTempModfile && s.view.workspaceMode&tempModfile != 0 {
 		modFH, err := s.GetFile(ctx, s.view.modURI)
 		if err != nil {
 			return "", nil, nil, cleanup, err
@@ -1229,3 +1232,66 @@
 	s.builtin = &builtinPackageHandle{handle: h}
 	return nil
 }
+
+const workspaceModuleVersion = "v0.0.0-00010101000000-000000000000"
+
+// buildWorkspaceModule generates a workspace module given the modules in the
+// the workspace.
+func (s *snapshot) buildWorkspaceModule(ctx context.Context) (*modfile.File, error) {
+	file := &modfile.File{}
+	file.AddModuleStmt("gopls-workspace")
+
+	paths := make(map[string]*module)
+	for _, mod := range s.view.modules {
+		fh, err := s.view.snapshot.GetFile(ctx, mod.modURI)
+		if err != nil {
+			return nil, err
+		}
+		parsed, err := s.ParseMod(ctx, fh)
+		if err != nil {
+			return nil, err
+		}
+		path := parsed.File.Module.Mod.Path
+		paths[path] = mod
+		file.AddNewRequire(path, workspaceModuleVersion, false)
+		if err := file.AddReplace(path, "", mod.rootURI.Filename(), ""); err != nil {
+			return nil, err
+		}
+	}
+	// Go back through all of the modules to handle any of their replace
+	// statements.
+	for _, module := range s.view.modules {
+		fh, err := s.view.snapshot.GetFile(ctx, module.modURI)
+		if err != nil {
+			return nil, err
+		}
+		pmf, err := s.view.snapshot.ParseMod(ctx, fh)
+		if err != nil {
+			return nil, err
+		}
+		// If any of the workspace modules have replace directives, they need
+		// to be reflected in the workspace module.
+		for _, rep := range pmf.File.Replace {
+			// Don't replace any modules that are in our workspace--we should
+			// always use the version in the workspace.
+			if _, ok := paths[rep.Old.Path]; ok {
+				continue
+			}
+			newPath := rep.New.Path
+			newVersion := rep.New.Version
+			// If a replace points to a module in the workspace, make sure we
+			// direct it to version of the module in the workspace.
+			if mod, ok := paths[rep.New.Path]; ok {
+				newPath = mod.rootURI.Filename()
+				newVersion = ""
+			} else if rep.New.Version == "" && !filepath.IsAbs(rep.New.Path) {
+				// Make any relative paths absolute.
+				newPath = filepath.Join(module.rootURI.Filename(), rep.New.Path)
+			}
+			if err := file.AddReplace(rep.Old.Path, rep.Old.Version, newPath, newVersion); err != nil {
+				return nil, err
+			}
+		}
+	}
+	return file, nil
+}
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 964dba1..40baa94 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -20,6 +20,7 @@
 	"sync"
 	"time"
 
+	"golang.org/x/mod/modfile"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/event/keys"
 	"golang.org/x/tools/internal/gocommand"
@@ -64,6 +65,16 @@
 	// is just the folder. If we are in module mode, this is the module root.
 	root span.URI
 
+	// TODO: The modules and workspaceModule fields should probably be moved to
+	// the snapshot and invalidated on file changes.
+
+	// modules is the set of modules currently in this workspace.
+	modules map[span.URI]*module
+
+	// workspaceModule is an in-memory representation of the go.mod file for
+	// the workspace module.
+	workspaceModule *modfile.File
+
 	// importsMu guards imports-related state, particularly the ProcessEnv.
 	importsMu sync.Mutex
 
@@ -122,9 +133,9 @@
 	// The real go.mod and go.sum files that are attributed to a view.
 	modURI, sumURI span.URI
 
-	// True if this view runs go commands using temporary mod files.
-	// Only possible with Go versions 1.14 and above.
-	tmpMod bool
+	// workspaceMode describes the way in which the view's workspace should be
+	// loaded.
+	workspaceMode workspaceMode
 
 	// hasGopackagesDriver is true if the user has a value set for the
 	// GOPACKAGESDRIVER environment variable or a gopackagesdriver binary on
@@ -139,6 +150,19 @@
 	goEnv map[string]string
 }
 
+type workspaceMode int
+
+const (
+	standard workspaceMode = 1 << iota
+
+	// tempModfile indicates whether or not the -modfile flag should be used.
+	tempModfile
+
+	// workspaceModule indicates support for the experimental workspace module
+	// feature.
+	workspaceModule
+)
+
 type builtinPackageHandle struct {
 	handle *memoize.Handle
 }
@@ -147,6 +171,10 @@
 	parsed *source.BuiltinPackage
 	err    error
 }
+type module struct {
+	rootURI        span.URI
+	modURI, sumURI span.URI
+}
 
 // fileBase holds the common functionality for all files.
 // It is intended to be embedded in the file implementations
@@ -436,7 +464,7 @@
 	v.optionsMu.Unlock()
 
 	// Add -modfile to the build flags, if we are using it.
-	if v.tmpMod && modFH != nil {
+	if v.workspaceMode&tempModfile != 0 && modFH != nil {
 		var tmpURI span.URI
 		tmpURI, cleanup, err = tempModFile(modFH, sumFH)
 		if err != nil {
@@ -643,7 +671,29 @@
 			}
 		}()
 
-		err := s.load(ctx, viewLoadScope("LOAD_VIEW"), packagePath("builtin"))
+		// If we have multiple modules, we need to load them by paths.
+		var scopes []interface{}
+		if len(v.modules) > 0 {
+			// TODO(rstambler): Retry the initial workspace load for whichever
+			// modules we failed to load.
+			for _, mod := range v.modules {
+				fh, err := s.GetFile(ctx, mod.modURI)
+				if err != nil {
+					v.initializedErr = err
+					continue
+				}
+				parsed, err := s.ParseMod(ctx, fh)
+				if err != nil {
+					v.initializedErr = err
+					continue
+				}
+				path := parsed.File.Module.Mod.Path
+				scopes = append(scopes, moduleLoadScope(path))
+			}
+		} else {
+			scopes = append(scopes, viewLoadScope("LOAD_VIEW"))
+		}
+		err := s.load(ctx, append(scopes, packagePath("builtin"))...)
 		if ctx.Err() != nil {
 			return
 		}
@@ -738,18 +788,15 @@
 		v.root = span.URIFromPath(filepath.Dir(v.modURI.Filename()))
 	}
 
-	// Now that we have set all required fields,
-	// check if the view has a valid build configuration.
-	v.setBuildConfiguration()
-
 	// The user has disabled the use of the -modfile flag or has no go.mod file.
 	if !options.TempModfile || v.modURI == "" {
 		return nil
 	}
+	v.workspaceMode = standard
 	if modfileFlag, err := v.modfileFlagExists(ctx, v.Options().Env); err != nil {
 		return err
 	} else if modfileFlag {
-		v.tmpMod = true
+		v.workspaceMode |= tempModfile
 	}
 	return nil
 }
@@ -770,10 +817,14 @@
 	if v.hasGopackagesDriver {
 		return true
 	}
-	// Check if the user is working within a module.
+	// Check if the user is working within a module or if we have found
+	// multiple modules in the workspace.
 	if v.modURI != "" {
 		return true
 	}
+	if len(v.modules) > 0 {
+		return true
+	}
 	// The user may have a multiple directories in their GOPATH.
 	// Check if the workspace is within any of them.
 	for _, gp := range filepath.SplitList(v.gopath) {
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index d9b90f8..7fb3a5c 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -51,6 +51,10 @@
 	var codeActions []protocol.CodeAction
 	switch fh.Kind() {
 	case source.Mod:
+		// TODO: Support code actions for views with multiple modules.
+		if snapshot.View().ModFile() == "" {
+			return nil, nil
+		}
 		if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 {
 			modQuickFixes, err := moduleQuickFixes(ctx, snapshot, diagnostics)
 			if err == source.ErrTmpModfileUnsupported {
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
index be07339..d5cd277 100644
--- a/internal/lsp/fake/editor.go
+++ b/internal/lsp/fake/editor.go
@@ -84,6 +84,10 @@
 
 	// EnableStaticcheck enables staticcheck analyzers.
 	EnableStaticcheck bool
+
+	// ExperimentalWorkspaceModule enables the experimental support for
+	// multi-module workspaces.
+	ExperimentalWorkspaceModule bool
 }
 
 // NewEditor Creates a new Editor.
@@ -192,6 +196,9 @@
 	if e.Config.EnableStaticcheck {
 		config["staticcheck"] = true
 	}
+	if e.Config.ExperimentalWorkspaceModule {
+		config["experimentalWorkspaceModule"] = true
+	}
 
 	return config
 }
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index aeba460..a1a415c 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -338,6 +338,10 @@
 	// modules containing the workspace folders. Set this to false to avoid loading
 	// your entire module. This is particularly useful for those working in a monorepo.
 	ExpandWorkspaceToModule bool
+
+	// ExperimentalWorkspaceModule opts a user into the experimental support
+	// for multi-module workspaces.
+	ExperimentalWorkspaceModule bool
 }
 
 // DebuggingOptions should not affect the logical execution of Gopls, but may
@@ -647,6 +651,9 @@
 	case "expandWorkspaceToModule":
 		result.setBool(&o.ExpandWorkspaceToModule)
 
+	case "experimentalWorkspaceModule":
+		result.setBool(&o.ExperimentalWorkspaceModule)
+
 	// Replaced settings.
 	case "experimentalDisabledAnalyses":
 		result.State = OptionDeprecated
diff --git a/internal/lsp/source/options_json.go b/internal/lsp/source/options_json.go
index 7007fb2..f6c7e06 100755
--- a/internal/lsp/source/options_json.go
+++ b/internal/lsp/source/options_json.go
@@ -2,4 +2,4 @@
 
 package source
 
-const OptionsJson = "{\"Debugging\":[{\"Name\":\"verboseOutput\",\"Type\":\"bool\",\"Doc\":\"verboseOutput enables additional debug logging.\\n\",\"Default\":\"false\"},{\"Name\":\"completionBudget\",\"Type\":\"time.Duration\",\"Doc\":\"completionBudget is the soft latency goal for completion requests. Most\\nrequests finish in a couple milliseconds, but in some cases deep\\ncompletions can take much longer. As we use up our budget we\\ndynamically reduce the search scope to ensure we return timely\\nresults. Zero means unlimited.\\n\",\"Default\":\"\\\"100ms\\\"\"},{\"Name\":\"literalCompletions\",\"Type\":\"bool\",\"Doc\":\"literalCompletions controls whether literal candidates such as\\n\\\"\\u0026someStruct{}\\\" are offered. Tests disable this flag to simplify\\ntheir expected values.\\n\",\"Default\":\"true\"}],\"Experimental\":[{\"Name\":\"analyses\",\"Type\":\"map[string]bool\",\"Doc\":\"analyses specify analyses that the user would like to enable or disable.\\nA map of the names of analysis passes that should be enabled/disabled.\\nA full list of analyzers that gopls uses can be found [here](analyzers.md)\\n\\nExample Usage:\\n```json5\\n...\\n\\\"analyses\\\": {\\n  \\\"unreachable\\\": false, // Disable the unreachable analyzer.\\n  \\\"unusedparams\\\": true  // Enable the unusedparams analyzer.\\n}\\n...\\n```\\n\",\"Default\":\"null\"},{\"Name\":\"codelens\",\"Type\":\"map[string]bool\",\"Doc\":\"overrides the enabled/disabled state of various code lenses. Currently, we\\nsupport several code lenses:\\n\\n* `generate`: run `go generate` as specified by a `//go:generate` directive.\\n* `upgrade_dependency`: upgrade a dependency listed in a `go.mod` file.\\n* `test`: run `go test -run` for a test func.\\n* `gc_details`: Show the gc compiler's choices for inline analysis and escaping.\\n\\nExample Usage:\\n```json5\\n\\\"gopls\\\": {\\n...\\n  \\\"codelens\\\": {\\n    \\\"generate\\\": false,  // Don't run `go generate`.\\n    \\\"gc_details\\\": true  // Show a code lens toggling the display of gc's choices.\\n  }\\n...\\n}\\n```\\n\",\"Default\":\"{\\\"gc_details\\\":false,\\\"generate\\\":true,\\\"regenerate_cgo\\\":true,\\\"tidy\\\":true,\\\"upgrade_dependency\\\":true,\\\"vendor\\\":true}\"},{\"Name\":\"completionDocumentation\",\"Type\":\"bool\",\"Doc\":\"completionDocumentation enables documentation with completion results.\\n\",\"Default\":\"true\"},{\"Name\":\"completeUnimported\",\"Type\":\"bool\",\"Doc\":\"completeUnimported enables completion for packages that you do not currently import.\\n\",\"Default\":\"true\"},{\"Name\":\"deepCompletion\",\"Type\":\"bool\",\"Doc\":\"deepCompletion If true, this turns on the ability to return completions from deep inside relevant entities, rather than just the locally accessible ones.\\n\\nConsider this example:\\n\\n```go\\npackage main\\n\\nimport \\\"fmt\\\"\\n\\ntype wrapString struct {\\n    str string\\n}\\n\\nfunc main() {\\n    x := wrapString{\\\"hello world\\\"}\\n    fmt.Printf(\\u003c\\u003e)\\n}\\n```\\n\\nAt the location of the `\\u003c\\u003e` in this program, deep completion would suggest the result `x.str`.\\n\",\"Default\":\"true\"},{\"Name\":\"matcher\",\"Type\":\"golang.org/x/tools/internal/lsp/source.Matcher\",\"Doc\":\"matcher sets the algorithm that is used when calculating completion candidates. Must be one of:\\n\\n* `\\\"fuzzy\\\"`\\n* `\\\"caseSensitive\\\"`\\n* `\\\"caseInsensitive\\\"`\\n\",\"Default\":\"\\\"Fuzzy\\\"\"},{\"Name\":\"annotations\",\"Type\":\"map[string]bool\",\"Doc\":\"annotations suppress various kinds of optimization diagnostics\\nthat would be reported by the gc_details command.\\n  noNilcheck suppresses display of nilchecks.\\n  noEscape suppresses escape choices.\\n  noInline suppresses inlining choices.\\n  noBounds suppresses bounds checking diagnositcs.\\n\",\"Default\":\"null\"},{\"Name\":\"staticcheck\",\"Type\":\"bool\",\"Doc\":\"staticcheck enables additional analyses from staticcheck.io.\\n\",\"Default\":\"false\"},{\"Name\":\"symbolMatcher\",\"Type\":\"golang.org/x/tools/internal/lsp/source.SymbolMatcher\",\"Doc\":\"symbolMatcher sets the algorithm that is used when finding workspace symbols. Must be one of:\\n\\n* `\\\"fuzzy\\\"`\\n* `\\\"caseSensitive\\\"`\\n* `\\\"caseInsensitive\\\"`\\n\",\"Default\":\"\\\"SymbolFuzzy\\\"\"},{\"Name\":\"symbolStyle\",\"Type\":\"golang.org/x/tools/internal/lsp/source.SymbolStyle\",\"Doc\":\"symbolStyle specifies what style of symbols to return in symbol requests. Must be one of:\\n\\n* `\\\"full\\\"`\\n* `\\\"dynamic\\\"`\\n* `\\\"package\\\"`\\n\",\"Default\":\"\\\"PackageQualifiedSymbols\\\"\"},{\"Name\":\"linksInHover\",\"Type\":\"bool\",\"Doc\":\"linksInHover toggles the presence of links to documentation in hover.\\n\",\"Default\":\"true\"},{\"Name\":\"tempModfile\",\"Type\":\"bool\",\"Doc\":\"tempModfile controls the use of the -modfile flag in Go 1.14.\\n\",\"Default\":\"true\"},{\"Name\":\"importShortcut\",\"Type\":\"golang.org/x/tools/internal/lsp/source.ImportShortcut\",\"Doc\":\"importShortcut specifies whether import statements should link to\\ndocumentation or go to definitions. Must be one of:\\n\\n* `\\\"both\\\"`\\n* `\\\"link\\\"`\\n* `\\\"definition\\\"`\\n\",\"Default\":\"\\\"Both\\\"\"},{\"Name\":\"verboseWorkDoneProgress\",\"Type\":\"bool\",\"Doc\":\"verboseWorkDoneProgress controls whether the LSP server should send\\nprogress reports for all work done outside the scope of an RPC.\\n\",\"Default\":\"false\"},{\"Name\":\"expandWorkspaceToModule\",\"Type\":\"bool\",\"Doc\":\"expandWorkspaceToModule instructs `gopls` to expand the scope of the workspace to include the\\nmodules containing the workspace folders. Set this to false to avoid loading\\nyour entire module. This is particularly useful for those working in a monorepo.\\n\",\"Default\":\"true\"}],\"User\":[{\"Name\":\"buildFlags\",\"Type\":\"[]string\",\"Doc\":\"buildFlags is the set of flags passed on to the build system when invoked.\\nIt is applied to queries like `go list`, which is used when discovering files.\\nThe most common use is to set `-tags`.\\n\",\"Default\":\"[]\"},{\"Name\":\"env\",\"Type\":\"[]string\",\"Doc\":\"env adds environment variables to external commands run by `gopls`, most notably `go list`.\\n\",\"Default\":\"[]\"},{\"Name\":\"hoverKind\",\"Type\":\"golang.org/x/tools/internal/lsp/source.HoverKind\",\"Doc\":\"hoverKind controls the information that appears in the hover text.\\nIt must be one of:\\n* `\\\"NoDocumentation\\\"`\\n* `\\\"SynopsisDocumentation\\\"`\\n* `\\\"FullDocumentation\\\"`\\n\\nAuthors of editor clients may wish to handle hover text differently, and so might use different settings. The options below are not intended for use by anyone other than the authors of editor plugins.\\n\\n* `\\\"SingleLine\\\"`\\n* `\\\"Structured\\\"`\\n\",\"Default\":\"\\\"FullDocumentation\\\"\"},{\"Name\":\"usePlaceholders\",\"Type\":\"bool\",\"Doc\":\"placeholders enables placeholders for function parameters or struct fields in completion responses.\\n\",\"Default\":\"false\"},{\"Name\":\"linkTarget\",\"Type\":\"string\",\"Doc\":\"linkTarget controls where documentation links go.\\nIt might be one of:\\n\\n* `\\\"godoc.org\\\"`\\n* `\\\"pkg.go.dev\\\"`\\n\\nIf company chooses to use its own `godoc.org`, its address can be used as well.\\n\",\"Default\":\"\\\"pkg.go.dev\\\"\"},{\"Name\":\"local\",\"Type\":\"string\",\"Doc\":\"local is the equivalent of the `goimports -local` flag, which puts imports beginning with this string after 3rd-party packages.\\nIt should be the prefix of the import path whose imports should be grouped separately.\\n\",\"Default\":\"\\\"\\\"\"},{\"Name\":\"gofumpt\",\"Type\":\"bool\",\"Doc\":\"gofumpt indicates if we should run gofumpt formatting.\\n\",\"Default\":\"false\"}]}"
+const OptionsJson = "{\"Debugging\":[{\"Name\":\"verboseOutput\",\"Type\":\"bool\",\"Doc\":\"verboseOutput enables additional debug logging.\\n\",\"Default\":\"false\"},{\"Name\":\"completionBudget\",\"Type\":\"time.Duration\",\"Doc\":\"completionBudget is the soft latency goal for completion requests. Most\\nrequests finish in a couple milliseconds, but in some cases deep\\ncompletions can take much longer. As we use up our budget we\\ndynamically reduce the search scope to ensure we return timely\\nresults. Zero means unlimited.\\n\",\"Default\":\"\\\"100ms\\\"\"},{\"Name\":\"literalCompletions\",\"Type\":\"bool\",\"Doc\":\"literalCompletions controls whether literal candidates such as\\n\\\"\\u0026someStruct{}\\\" are offered. Tests disable this flag to simplify\\ntheir expected values.\\n\",\"Default\":\"true\"}],\"Experimental\":[{\"Name\":\"analyses\",\"Type\":\"map[string]bool\",\"Doc\":\"analyses specify analyses that the user would like to enable or disable.\\nA map of the names of analysis passes that should be enabled/disabled.\\nA full list of analyzers that gopls uses can be found [here](analyzers.md)\\n\\nExample Usage:\\n```json5\\n...\\n\\\"analyses\\\": {\\n  \\\"unreachable\\\": false, // Disable the unreachable analyzer.\\n  \\\"unusedparams\\\": true  // Enable the unusedparams analyzer.\\n}\\n...\\n```\\n\",\"Default\":\"null\"},{\"Name\":\"codelens\",\"Type\":\"map[string]bool\",\"Doc\":\"overrides the enabled/disabled state of various code lenses. Currently, we\\nsupport several code lenses:\\n\\n* `generate`: run `go generate` as specified by a `//go:generate` directive.\\n* `upgrade_dependency`: upgrade a dependency listed in a `go.mod` file.\\n* `test`: run `go test -run` for a test func.\\n* `gc_details`: Show the gc compiler's choices for inline analysis and escaping.\\n\\nExample Usage:\\n```json5\\n\\\"gopls\\\": {\\n...\\n  \\\"codelens\\\": {\\n    \\\"generate\\\": false,  // Don't run `go generate`.\\n    \\\"gc_details\\\": true  // Show a code lens toggling the display of gc's choices.\\n  }\\n...\\n}\\n```\\n\",\"Default\":\"{\\\"gc_details\\\":false,\\\"generate\\\":true,\\\"regenerate_cgo\\\":true,\\\"tidy\\\":true,\\\"upgrade_dependency\\\":true,\\\"vendor\\\":true}\"},{\"Name\":\"completionDocumentation\",\"Type\":\"bool\",\"Doc\":\"completionDocumentation enables documentation with completion results.\\n\",\"Default\":\"true\"},{\"Name\":\"completeUnimported\",\"Type\":\"bool\",\"Doc\":\"completeUnimported enables completion for packages that you do not currently import.\\n\",\"Default\":\"true\"},{\"Name\":\"deepCompletion\",\"Type\":\"bool\",\"Doc\":\"deepCompletion If true, this turns on the ability to return completions from deep inside relevant entities, rather than just the locally accessible ones.\\n\\nConsider this example:\\n\\n```go\\npackage main\\n\\nimport \\\"fmt\\\"\\n\\ntype wrapString struct {\\n    str string\\n}\\n\\nfunc main() {\\n    x := wrapString{\\\"hello world\\\"}\\n    fmt.Printf(\\u003c\\u003e)\\n}\\n```\\n\\nAt the location of the `\\u003c\\u003e` in this program, deep completion would suggest the result `x.str`.\\n\",\"Default\":\"true\"},{\"Name\":\"matcher\",\"Type\":\"golang.org/x/tools/internal/lsp/source.Matcher\",\"Doc\":\"matcher sets the algorithm that is used when calculating completion candidates. Must be one of:\\n\\n* `\\\"fuzzy\\\"`\\n* `\\\"caseSensitive\\\"`\\n* `\\\"caseInsensitive\\\"`\\n\",\"Default\":\"\\\"Fuzzy\\\"\"},{\"Name\":\"annotations\",\"Type\":\"map[string]bool\",\"Doc\":\"annotations suppress various kinds of optimization diagnostics\\nthat would be reported by the gc_details command.\\n  noNilcheck suppresses display of nilchecks.\\n  noEscape suppresses escape choices.\\n  noInline suppresses inlining choices.\\n  noBounds suppresses bounds checking diagnositcs.\\n\",\"Default\":\"null\"},{\"Name\":\"staticcheck\",\"Type\":\"bool\",\"Doc\":\"staticcheck enables additional analyses from staticcheck.io.\\n\",\"Default\":\"false\"},{\"Name\":\"symbolMatcher\",\"Type\":\"golang.org/x/tools/internal/lsp/source.SymbolMatcher\",\"Doc\":\"symbolMatcher sets the algorithm that is used when finding workspace symbols. Must be one of:\\n\\n* `\\\"fuzzy\\\"`\\n* `\\\"caseSensitive\\\"`\\n* `\\\"caseInsensitive\\\"`\\n\",\"Default\":\"\\\"SymbolFuzzy\\\"\"},{\"Name\":\"symbolStyle\",\"Type\":\"golang.org/x/tools/internal/lsp/source.SymbolStyle\",\"Doc\":\"symbolStyle specifies what style of symbols to return in symbol requests. Must be one of:\\n\\n* `\\\"full\\\"`\\n* `\\\"dynamic\\\"`\\n* `\\\"package\\\"`\\n\",\"Default\":\"\\\"PackageQualifiedSymbols\\\"\"},{\"Name\":\"linksInHover\",\"Type\":\"bool\",\"Doc\":\"linksInHover toggles the presence of links to documentation in hover.\\n\",\"Default\":\"true\"},{\"Name\":\"tempModfile\",\"Type\":\"bool\",\"Doc\":\"tempModfile controls the use of the -modfile flag in Go 1.14.\\n\",\"Default\":\"true\"},{\"Name\":\"importShortcut\",\"Type\":\"golang.org/x/tools/internal/lsp/source.ImportShortcut\",\"Doc\":\"importShortcut specifies whether import statements should link to\\ndocumentation or go to definitions. Must be one of:\\n\\n* `\\\"both\\\"`\\n* `\\\"link\\\"`\\n* `\\\"definition\\\"`\\n\",\"Default\":\"\\\"Both\\\"\"},{\"Name\":\"verboseWorkDoneProgress\",\"Type\":\"bool\",\"Doc\":\"verboseWorkDoneProgress controls whether the LSP server should send\\nprogress reports for all work done outside the scope of an RPC.\\n\",\"Default\":\"false\"},{\"Name\":\"expandWorkspaceToModule\",\"Type\":\"bool\",\"Doc\":\"expandWorkspaceToModule instructs `gopls` to expand the scope of the workspace to include the\\nmodules containing the workspace folders. Set this to false to avoid loading\\nyour entire module. This is particularly useful for those working in a monorepo.\\n\",\"Default\":\"true\"},{\"Name\":\"experimentalWorkspaceModule\",\"Type\":\"bool\",\"Doc\":\"experimentalWorkspaceModule opts a user into the experimental support\\nfor multi-module workspaces.\\n\",\"Default\":\"false\"}],\"User\":[{\"Name\":\"buildFlags\",\"Type\":\"[]string\",\"Doc\":\"buildFlags is the set of flags passed on to the build system when invoked.\\nIt is applied to queries like `go list`, which is used when discovering files.\\nThe most common use is to set `-tags`.\\n\",\"Default\":\"[]\"},{\"Name\":\"env\",\"Type\":\"[]string\",\"Doc\":\"env adds environment variables to external commands run by `gopls`, most notably `go list`.\\n\",\"Default\":\"[]\"},{\"Name\":\"hoverKind\",\"Type\":\"golang.org/x/tools/internal/lsp/source.HoverKind\",\"Doc\":\"hoverKind controls the information that appears in the hover text.\\nIt must be one of:\\n* `\\\"NoDocumentation\\\"`\\n* `\\\"SynopsisDocumentation\\\"`\\n* `\\\"FullDocumentation\\\"`\\n\\nAuthors of editor clients may wish to handle hover text differently, and so might use different settings. The options below are not intended for use by anyone other than the authors of editor plugins.\\n\\n* `\\\"SingleLine\\\"`\\n* `\\\"Structured\\\"`\\n\",\"Default\":\"\\\"FullDocumentation\\\"\"},{\"Name\":\"usePlaceholders\",\"Type\":\"bool\",\"Doc\":\"placeholders enables placeholders for function parameters or struct fields in completion responses.\\n\",\"Default\":\"false\"},{\"Name\":\"linkTarget\",\"Type\":\"string\",\"Doc\":\"linkTarget controls where documentation links go.\\nIt might be one of:\\n\\n* `\\\"godoc.org\\\"`\\n* `\\\"pkg.go.dev\\\"`\\n\\nIf company chooses to use its own `godoc.org`, its address can be used as well.\\n\",\"Default\":\"\\\"pkg.go.dev\\\"\"},{\"Name\":\"local\",\"Type\":\"string\",\"Doc\":\"local is the equivalent of the `goimports -local` flag, which puts imports beginning with this string after 3rd-party packages.\\nIt should be the prefix of the import path whose imports should be grouped separately.\\n\",\"Default\":\"\\\"\\\"\"},{\"Name\":\"gofumpt\",\"Type\":\"bool\",\"Doc\":\"gofumpt indicates if we should run gofumpt formatting.\\n\",\"Default\":\"false\"}]}"