internal/lsp: handle modifications to the workspace module

We should be able to smoothly add and remove modules from the workspace.
This change moves the module-related fields from the view into the
snapshot so that they can be easily modified and recomputed. The
workspace module is now a workspaceModuleHandle.

Updates golang/go#32394

Change-Id: I6ade7f223cc6070a29b6021b825586b753a0daf1
Reviewed-on: https://go-review.googlesource.com/c/tools/+/254940
Trust: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/regtest/modfile_test.go b/gopls/internal/regtest/modfile_test.go
index a7f33ab..5ec9f23 100644
--- a/gopls/internal/regtest/modfile_test.go
+++ b/gopls/internal/regtest/modfile_test.go
@@ -74,7 +74,7 @@
 
 	// Reproduce golang/go#40269 by deleting and recreating main.go.
 	t.Run("delete main.go", func(t *testing.T) {
-		t.Skipf("This test will be flaky until golang/go#40269 is resolved.")
+		t.Skip("This test will be flaky until golang/go#40269 is resolved.")
 
 		withOptions(WithProxyFiles(proxy)).run(t, untidyModule, func(t *testing.T, env *Env) {
 			goModContent := env.ReadWorkspaceFile("go.mod")
diff --git a/gopls/internal/regtest/watch_test.go b/gopls/internal/regtest/watch_test.go
index 50a628a..8ed4f7b 100644
--- a/gopls/internal/regtest/watch_test.go
+++ b/gopls/internal/regtest/watch_test.go
@@ -185,7 +185,7 @@
 }
 `
 	runner.Run(t, missing, func(t *testing.T, env *Env) {
-		t.Skipf("the initial workspace load fails and never retries")
+		t.Skip("the initial workspace load fails and never retries")
 
 		env.Await(
 			env.DiagnosticAtRegexp("a/a.go", "\"mod.com/c\""),
@@ -586,9 +586,9 @@
 	})
 }
 
-// Reproduces golang/go#37069.
+// Reproduces golang/go#40340.
 func TestSwitchFromGOPATHToModules(t *testing.T) {
-	t.Skipf("golang/go#37069 is not yet resolved.")
+	t.Skip("golang/go#40340 is not yet resolved.")
 
 	const files = `
 -- foo/blah/blah.go --
diff --git a/gopls/internal/regtest/workspace_test.go b/gopls/internal/regtest/workspace_test.go
index d672316..2c1a1d9 100644
--- a/gopls/internal/regtest/workspace_test.go
+++ b/gopls/internal/regtest/workspace_test.go
@@ -6,6 +6,7 @@
 
 import (
 	"fmt"
+	"strings"
 	"testing"
 
 	"golang.org/x/tools/internal/lsp"
@@ -205,3 +206,108 @@
 		)
 	})
 }
+
+// This change tests that the version of the module used changes after it has
+// been deleted from the workspace.
+func TestDeleteModule_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),
+	).run(t, multiModule, func(t *testing.T, env *Env) {
+		env.Await(InitialWorkspaceLoad)
+		env.OpenFile("moda/a/a.go")
+
+		original, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
+		if want := "modb/b/b.go"; !strings.HasSuffix(original, want) {
+			t.Errorf("expected %s, got %v", want, original)
+		}
+		env.CloseBuffer(original)
+		env.RemoveWorkspaceFile("modb/b/b.go")
+		env.RemoveWorkspaceFile("modb/go.mod")
+		env.Await(
+			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2),
+		)
+		got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
+		if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(got, want) {
+			t.Errorf("expected %s, got %v", want, got)
+		}
+	})
+}
+
+// This change tests that the version of the module used changes after it has
+// been added to the workspace.
+func TestCreateModule_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()
+}
+`
+	withOptions(
+		WithProxyFiles(workspaceModuleProxy),
+	).run(t, multiModule, func(t *testing.T, env *Env) {
+		env.Await(InitialWorkspaceLoad)
+		env.OpenFile("moda/a/a.go")
+		original, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
+		if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(original, want) {
+			t.Errorf("expected %s, got %v", want, original)
+		}
+		env.WriteWorkspaceFiles(map[string]string{
+			"modb/go.mod": "module b.com",
+			"modb/b/b.go": `package b
+
+func Hello() int {
+	var x int
+}
+`,
+		})
+		env.Await(
+			OnceMet(
+				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+				env.DiagnosticAtRegexp("modb/b/b.go", "x"),
+			),
+		)
+		got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
+		if want := "modb/b/b.go"; !strings.HasSuffix(got, want) {
+			t.Errorf("expected %s, got %v", want, original)
+		}
+	})
+}
diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go
index 48b0912..7bea132 100644
--- a/internal/lsp/cache/check.go
+++ b/internal/lsp/cache/check.go
@@ -177,8 +177,7 @@
 		b.WriteString(string(dep))
 	}
 	for _, cgf := range pghs {
-		b.WriteString(string(cgf.file.URI()))
-		b.WriteString(cgf.file.FileIdentity().Hash)
+		b.WriteString(cgf.file.FileIdentity().String())
 	}
 	return packageHandleKey(hashContents(b.Bytes()))
 }
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index 0dc975a..52568e7 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -199,13 +199,18 @@
 // 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 {
+	if len(s.modules) == 0 {
 		return "", cleanup, nil
 	}
-	if s.view.workspaceModule == nil {
-		return "", cleanup, nil
+	wsModuleHandle, err := s.getWorkspaceModuleHandle(ctx)
+	if err != nil {
+		return "", nil, err
 	}
-	content, err := s.view.workspaceModule.Format()
+	file, err := wsModuleHandle.build(ctx, s)
+	if err != nil {
+		return "", nil, err
+	}
+	content, err := file.Format()
 	if err != nil {
 		return "", cleanup, err
 	}
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 28aae96..597e46b 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -7,8 +7,6 @@
 import (
 	"context"
 	"fmt"
-	"os"
-	"path/filepath"
 	"strconv"
 	"strings"
 	"sync"
@@ -173,7 +171,6 @@
 		name:               name,
 		folder:             folder,
 		root:               folder,
-		modules:            make(map[span.URI]*moduleRoot),
 		filesByURI:         make(map[span.URI]*fileBase),
 		filesByBase:        make(map[string][]*fileBase),
 	}
@@ -194,6 +191,7 @@
 		modTidyHandles:    make(map[span.URI]*modTidyHandle),
 		modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
 		modWhyHandles:     make(map[span.URI]*modWhyHandle),
+		modules:           make(map[span.URI]*moduleRoot),
 	}
 
 	if v.session.cache.options != nil {
@@ -206,7 +204,7 @@
 	}
 
 	// Find all of the modules in the workspace.
-	if err := v.findWorkspaceModules(ctx, options); err != nil {
+	if err := v.snapshot.findWorkspaceModules(ctx, options); err != nil {
 		return nil, nil, func() {}, err
 	}
 
@@ -214,11 +212,9 @@
 	// check if the view has a valid build configuration.
 	v.setBuildConfiguration()
 
-	// Build the workspace module, if needed.
-	if options.ExperimentalWorkspaceModule {
-		if err := v.buildWorkspaceModule(ctx); err != nil {
-			return nil, nil, func() {}, err
-		}
+	// Decide if we should use the workspace module.
+	if v.determineWorkspaceModuleLocked() {
+		v.workspaceMode |= usesWorkspaceModule | moduleMode
 	}
 
 	// We have v.goEnv now.
@@ -242,94 +238,12 @@
 	snapshot := v.snapshot
 	release := snapshot.generation.Acquire(initCtx)
 	go func() {
-		v.initialize(initCtx, snapshot, true)
+		snapshot.initialize(initCtx, true)
 		release()
 	}()
 	return v, snapshot, snapshot.generation.Acquire(ctx), nil
 }
 
-// findWorkspaceModules 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) findWorkspaceModules(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
-	}
-
-	// Walk the view's folder to find all modules in the view.
-	root := v.root.Filename()
-	return 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] = &moduleRoot{
-			rootURI: rootURI,
-			modURI:  modURI,
-			sumURI:  span.URIFromPath(sumFilename(modURI)),
-		}
-		return nil
-	})
-}
-
-func (v *View) buildWorkspaceModule(ctx context.Context) error {
-	// If the view has an invalid configuration, don't build the workspace
-	// module.
-	if !v.hasValidBuildConfiguration {
-		return nil
-	}
-	// If the view is not in a module and contains no modules, but still has a
-	// valid workspace configuration, do not create the workspace module.
-	// It could be using GOPATH or a different build system entirely.
-	if v.modURI == "" && len(v.modules) == 0 && v.hasValidBuildConfiguration {
-		return nil
-	}
-	v.workspaceMode |= moduleMode
-
-	// Don't default to multi-workspace mode if one of the modules contains a
-	// vendor directory. We still have to decide how to handle vendoring.
-	for _, mod := range v.modules {
-		if info, _ := os.Stat(filepath.Join(mod.rootURI.Filename(), "vendor")); info != nil {
-			return nil
-		}
-	}
-
-	v.workspaceMode |= usesWorkspaceModule
-
-	// 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 14c575d..3d5db7a 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -94,6 +94,13 @@
 	modTidyHandles    map[span.URI]*modTidyHandle
 	modUpgradeHandles map[span.URI]*modUpgradeHandle
 	modWhyHandles     map[span.URI]*modWhyHandle
+
+	// modules is the set of modules currently in this workspace.
+	modules map[span.URI]*moduleRoot
+
+	// workspaceModuleHandle keeps track of the in-memory representation of the
+	// go.mod file for the workspace module.
+	workspaceModuleHandle *workspaceModuleHandle
 }
 
 type packageKey struct {
@@ -673,12 +680,15 @@
 
 	s.mu.Lock()
 	defer s.mu.Unlock()
+	return s.getFileLocked(ctx, f)
+}
 
+func (s *snapshot) getFileLocked(ctx context.Context, f *fileBase) (source.VersionedFileHandle, error) {
 	if fh, ok := s.files[f.URI()]; ok {
 		return fh, nil
 	}
 
-	fh, err := s.view.session.cache.getFile(ctx, uri)
+	fh, err := s.view.session.cache.getFile(ctx, f.URI())
 	if err != nil {
 		return nil, err
 	}
@@ -732,7 +742,7 @@
 	}
 	// We typically prefer to run something as intensive as the IWL without
 	// blocking. I'm not sure if there is a way to do that here.
-	s.view.initialize(ctx, s, false)
+	s.initialize(ctx, false)
 }
 
 // reloadWorkspace reloads the metadata for all invalidated workspace packages.
@@ -851,24 +861,26 @@
 
 	newGen := s.view.session.cache.store.Generation(generationName(s.view, s.id+1))
 	result := &snapshot{
-		id:                   s.id + 1,
-		generation:           newGen,
-		view:                 s.view,
-		builtin:              s.builtin,
-		ids:                  make(map[span.URI][]packageID),
-		importedBy:           make(map[packageID][]packageID),
-		metadata:             make(map[packageID]*metadata),
-		packages:             make(map[packageKey]*packageHandle),
-		actions:              make(map[actionKey]*actionHandle),
-		files:                make(map[span.URI]source.VersionedFileHandle),
-		goFiles:              make(map[parseKey]*parseGoHandle),
-		workspaceDirectories: make(map[span.URI]struct{}),
-		workspacePackages:    make(map[packageID]packagePath),
-		unloadableFiles:      make(map[span.URI]struct{}),
-		parseModHandles:      make(map[span.URI]*parseModHandle),
-		modTidyHandles:       make(map[span.URI]*modTidyHandle),
-		modUpgradeHandles:    make(map[span.URI]*modUpgradeHandle),
-		modWhyHandles:        make(map[span.URI]*modWhyHandle),
+		id:                    s.id + 1,
+		generation:            newGen,
+		view:                  s.view,
+		builtin:               s.builtin,
+		ids:                   make(map[span.URI][]packageID),
+		importedBy:            make(map[packageID][]packageID),
+		metadata:              make(map[packageID]*metadata),
+		packages:              make(map[packageKey]*packageHandle),
+		actions:               make(map[actionKey]*actionHandle),
+		files:                 make(map[span.URI]source.VersionedFileHandle),
+		goFiles:               make(map[parseKey]*parseGoHandle),
+		workspaceDirectories:  make(map[span.URI]struct{}),
+		workspacePackages:     make(map[packageID]packagePath),
+		unloadableFiles:       make(map[span.URI]struct{}),
+		parseModHandles:       make(map[span.URI]*parseModHandle),
+		modTidyHandles:        make(map[span.URI]*modTidyHandle),
+		modUpgradeHandles:     make(map[span.URI]*modUpgradeHandle),
+		modWhyHandles:         make(map[span.URI]*modWhyHandle),
+		modules:               make(map[span.URI]*moduleRoot),
+		workspaceModuleHandle: s.workspaceModuleHandle,
 	}
 
 	if s.builtin != nil {
@@ -885,7 +897,6 @@
 	}
 	// Copy all of the modHandles.
 	for k, v := range s.parseModHandles {
-		newGen.Inherit(v.handle)
 		result.parseModHandles[k] = v
 	}
 	// Copy all of the workspace directories. They may be reset later.
@@ -923,6 +934,11 @@
 		result.modWhyHandles[k] = v
 	}
 
+	// Add all of the modules now. They may be deleted or added to later.
+	for k, v := range s.modules {
+		result.modules[k] = v
+	}
+
 	// transitiveIDs keeps track of transitive reverse dependencies.
 	// If an ID is present in the map, invalidate its types.
 	// If an ID's value is true, invalidate its metadata too.
@@ -957,24 +973,69 @@
 				delete(result.modWhyHandles, k)
 			}
 		}
-		if currentFH.Kind() == source.Mod {
-			// If the view's go.mod file's contents have changed, invalidate the
-			// metadata for every known package in the snapshot.
+		currentExists := currentFH.URI() != ""
+		if currentExists {
+			if _, err := currentFH.Read(); os.IsNotExist(err) {
+				currentExists = false
+			}
+		}
+		// If the file invalidation is for a go.mod. originalFH is nil if the
+		// file is newly created.
+		currentMod := currentExists && currentFH.Kind() == source.Mod
+		originalMod := originalFH != nil && originalFH.Kind() == source.Mod
+		if currentMod || originalMod {
+			// If the view's go.mod file's contents have changed, invalidate
+			// the metadata for every known package in the snapshot.
 			if invalidateMetadata {
 				for k := range s.packages {
 					directIDs[k.id] = struct{}{}
 				}
+				// If a go.mod file in the workspace has changed, we need to
+				// rebuild the workspace module.
+				result.workspaceModuleHandle = nil
 			}
-
 			delete(result.parseModHandles, withoutURI)
 
-			if currentFH.URI() == s.view.modURI {
-				// The go.mod's replace directives may have changed. We may
-				// need to update our set of workspace directories. Use the new
-				// snapshot, as it can be locked without causing issues.
-				result.workspaceDirectories = result.findWorkspaceDirectories(ctx, currentFH)
+			// Check if this is a newly created go.mod file. When a new module
+			// is created, we have to retry the initial workspace load.
+			rootURI := span.URIFromPath(filepath.Dir(withoutURI.Filename()))
+			if currentMod {
+				if _, ok := result.modules[rootURI]; !ok {
+					result.addModule(ctx, currentFH.URI())
+					result.view.definitelyReinitialize()
+				}
+			} else if originalMod {
+				// Similarly, we need to retry the IWL if a go.mod in the workspace
+				// was deleted.
+				if _, ok := result.modules[rootURI]; ok {
+					delete(result.modules, rootURI)
+					result.view.definitelyReinitialize()
+				}
 			}
 		}
+		// Keep track of the creations and deletions of go.sum files.
+		// Creating a go.sum without an associated go.mod has no effect on the
+		// set of modules.
+		currentSum := currentExists && currentFH.Kind() == source.Sum
+		originalSum := originalFH != nil && originalFH.Kind() == source.Sum
+		if currentSum || originalSum {
+			rootURI := span.URIFromPath(filepath.Dir(withoutURI.Filename()))
+			if currentSum {
+				if mod, ok := result.modules[rootURI]; ok {
+					mod.sumURI = currentFH.URI()
+				}
+			} else if originalSum {
+				if mod, ok := result.modules[rootURI]; ok {
+					mod.sumURI = ""
+				}
+			}
+		}
+		if withoutURI == s.view.modURI {
+			// The go.mod's replace directives may have changed. We may
+			// need to update our set of workspace directories. Use the new
+			// snapshot, as it can be locked without causing issues.
+			result.workspaceDirectories = result.findWorkspaceDirectories(ctx, currentFH)
+		}
 
 		// If this is a file we don't yet know about,
 		// then we do not yet know what packages it should belong to.
@@ -1017,7 +1078,7 @@
 		}
 
 		// Handle the invalidated file; it may have new contents or not exist.
-		if _, err := currentFH.Read(); os.IsNotExist(err) {
+		if !currentExists {
 			delete(result.files, withoutURI)
 		} else {
 			result.files[withoutURI] = currentFH
@@ -1087,15 +1148,21 @@
 	}
 
 	// Inherit all of the go.mod-related handles.
-	for _, v := range s.modTidyHandles {
+	for _, v := range result.modTidyHandles {
 		newGen.Inherit(v.handle)
 	}
-	for _, v := range s.modUpgradeHandles {
+	for _, v := range result.modUpgradeHandles {
 		newGen.Inherit(v.handle)
 	}
-	for _, v := range s.modWhyHandles {
+	for _, v := range result.modWhyHandles {
 		newGen.Inherit(v.handle)
 	}
+	for _, v := range result.parseModHandles {
+		newGen.Inherit(v.handle)
+	}
+	if result.workspaceModuleHandle != nil {
+		newGen.Inherit(result.workspaceModuleHandle.handle)
+	}
 
 	// Don't bother copying the importedBy graph,
 	// as it changes each time we update metadata.
@@ -1128,9 +1195,9 @@
 	if originalFH.FileIdentity() == currentFH.FileIdentity() {
 		return false
 	}
-	// If a go.mod file's contents have changed, always invalidate metadata.
+	// If a go.mod in the workspace has been changed, invalidate metadata.
 	if kind := originalFH.Kind(); kind == source.Mod {
-		return originalFH.URI() == s.view.modURI
+		return isSubdirectory(filepath.Dir(s.view.root.Filename()), filepath.Dir(originalFH.URI().Filename()))
 	}
 	// Get the original and current parsed files in order to check package name
 	// and imports. Use the new snapshot to parse to avoid modifying the
@@ -1263,6 +1330,64 @@
 	return nil
 }
 
+type workspaceModuleHandle struct {
+	handle *memoize.Handle
+}
+
+type workspaceModuleData struct {
+	file *modfile.File
+	err  error
+}
+
+type workspaceModuleKey string
+
+func (wmh *workspaceModuleHandle) build(ctx context.Context, snapshot *snapshot) (*modfile.File, error) {
+	v, err := wmh.handle.Get(ctx, snapshot.generation, snapshot)
+	if err != nil {
+		return nil, err
+	}
+	data := v.(*workspaceModuleData)
+	return data.file, data.err
+}
+
+func (s *snapshot) getWorkspaceModuleHandle(ctx context.Context) (*workspaceModuleHandle, error) {
+	s.mu.Lock()
+	wsModule := s.workspaceModuleHandle
+	s.mu.Unlock()
+	if wsModule != nil {
+		return wsModule, nil
+	}
+	var fhs []source.FileHandle
+	for _, mod := range s.modules {
+		fh, err := s.GetFile(ctx, mod.modURI)
+		if err != nil {
+			return nil, err
+		}
+		fhs = append(fhs, fh)
+	}
+	sort.Slice(fhs, func(i, j int) bool {
+		return fhs[i].URI() < fhs[j].URI()
+	})
+	var k string
+	for _, fh := range fhs {
+		k += fh.FileIdentity().String()
+	}
+	key := workspaceModuleKey(hashContents([]byte(k)))
+	h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
+		s := arg.(*snapshot)
+		data := &workspaceModuleData{}
+		data.file, data.err = s.buildWorkspaceModule(ctx)
+		return data
+	})
+	wsModule = &workspaceModuleHandle{
+		handle: h,
+	}
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.workspaceModuleHandle = wsModule
+	return s.workspaceModuleHandle, nil
+}
+
 // buildWorkspaceModule generates a workspace module given the modules in the
 // the workspace.
 func (s *snapshot) buildWorkspaceModule(ctx context.Context) (*modfile.File, error) {
@@ -1270,8 +1395,8 @@
 	file.AddModuleStmt("gopls-workspace")
 
 	paths := make(map[string]*moduleRoot)
-	for _, mod := range s.view.modules {
-		fh, err := s.view.snapshot.GetFile(ctx, mod.modURI)
+	for _, mod := range s.modules {
+		fh, err := s.GetFile(ctx, mod.modURI)
 		if err != nil {
 			return nil, err
 		}
@@ -1291,12 +1416,12 @@
 	}
 	// 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)
+	for _, module := range s.modules {
+		fh, err := s.GetFile(ctx, module.modURI)
 		if err != nil {
 			return nil, err
 		}
-		pmf, err := s.view.snapshot.ParseMod(ctx, fh)
+		pmf, err := s.ParseMod(ctx, fh)
 		if err != nil {
 			return nil, err
 		}
@@ -1326,3 +1451,52 @@
 	}
 	return file, nil
 }
+
+// findWorkspaceModules 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 (s *snapshot) findWorkspaceModules(ctx context.Context, options *source.Options) error {
+	// Walk the view's folder to find all modules in the view.
+	root := s.view.root.Filename()
+	return 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)
+		s.addModule(ctx, modURI)
+		return nil
+	})
+}
+
+func (s *snapshot) addModule(ctx context.Context, modURI span.URI) {
+	rootURI := span.URIFromPath(filepath.Dir(modURI.Filename()))
+	sumURI := span.URIFromPath(sumFilename(modURI))
+	if info, _ := os.Stat(sumURI.Filename()); info == nil {
+		sumURI = ""
+	}
+	s.modules[rootURI] = &moduleRoot{
+		rootURI: rootURI,
+		modURI:  modURI,
+		sumURI:  sumURI,
+	}
+}
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 9412d88..d2cf068 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -67,16 +67,6 @@
 	// 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]*moduleRoot
-
-	// 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
 
@@ -687,42 +677,42 @@
 	return v.snapshot, v.snapshot.generation.Acquire(ctx)
 }
 
-func (v *View) initialize(ctx context.Context, s *snapshot, firstAttempt bool) {
+func (s *snapshot) initialize(ctx context.Context, firstAttempt bool) {
 	select {
 	case <-ctx.Done():
 		return
-	case v.initializationSema <- struct{}{}:
+	case s.view.initializationSema <- struct{}{}:
 	}
 
 	defer func() {
-		<-v.initializationSema
+		<-s.view.initializationSema
 	}()
 
-	if v.initializeOnce == nil {
+	if s.view.initializeOnce == nil {
 		return
 	}
-	v.initializeOnce.Do(func() {
+	s.view.initializeOnce.Do(func() {
 		defer func() {
-			v.initializeOnce = nil
+			s.view.initializeOnce = nil
 			if firstAttempt {
-				close(v.initialized)
+				close(s.view.initialized)
 			}
 		}()
 
 		// If we have multiple modules, we need to load them by paths.
 		var scopes []interface{}
-		if len(v.modules) > 0 {
+		if len(s.modules) > 0 {
 			// TODO(rstambler): Retry the initial workspace load for whichever
 			// modules we failed to load.
-			for _, mod := range v.modules {
+			for _, mod := range s.modules {
 				fh, err := s.GetFile(ctx, mod.modURI)
 				if err != nil {
-					v.initializedErr = err
+					s.view.initializedErr = err
 					continue
 				}
 				parsed, err := s.ParseMod(ctx, fh)
 				if err != nil {
-					v.initializedErr = err
+					s.view.initializedErr = err
 					continue
 				}
 				path := parsed.File.Module.Mod.Path
@@ -738,7 +728,7 @@
 		if err != nil {
 			event.Error(ctx, "initial workspace load failed", err)
 		}
-		v.initializedErr = err
+		s.view.initializedErr = err
 	})
 }
 
@@ -779,12 +769,20 @@
 }
 
 func (v *View) maybeReinitialize() {
+	v.reinitialize(false)
+}
+
+func (v *View) definitelyReinitialize() {
+	v.reinitialize(true)
+}
+
+func (v *View) reinitialize(force bool) {
 	v.initializationSema <- struct{}{}
 	defer func() {
 		<-v.initializationSema
 	}()
 
-	if v.initializedErr == nil {
+	if !force && v.initializedErr == nil {
 		return
 	}
 	var once sync.Once
@@ -856,6 +854,38 @@
 	return nil
 }
 
+func (v *View) determineWorkspaceModuleLocked() bool {
+	// If the user is intentionally limiting their workspace scope, add their
+	// folder to the roots and return early.
+	if !v.options.ExpandWorkspaceToModule {
+		return false
+	}
+	// The workspace module has been disabled by the user.
+	if !v.options.ExperimentalWorkspaceModule {
+		return false
+	}
+	// If the view has an invalid configuration, don't build the workspace
+	// module.
+	if !v.hasValidBuildConfiguration {
+		return false
+	}
+	// If the view is not in a module and contains no modules, but still has a
+	// valid workspace configuration, do not create the workspace module.
+	// It could be using GOPATH or a different build system entirely.
+	if v.modURI == "" && len(v.snapshot.modules) == 0 && v.hasValidBuildConfiguration {
+		return false
+	}
+
+	// Don't default to multi-workspace mode if one of the modules contains a
+	// vendor directory. We still have to decide how to handle vendoring.
+	for _, mod := range v.snapshot.modules {
+		if info, _ := os.Stat(filepath.Join(mod.rootURI.Filename(), "vendor")); info != nil {
+			return false
+		}
+	}
+	return true
+}
+
 func (v *View) setBuildConfiguration() (isValid bool) {
 	defer func() {
 		v.hasValidBuildConfiguration = isValid
@@ -870,7 +900,7 @@
 	if v.modURI != "" {
 		return true
 	}
-	if len(v.modules) > 0 {
+	if len(v.snapshot.modules) > 0 {
 		return true
 	}
 	// The user may have a multiple directories in their GOPATH.
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index f8f6bdd..d534eb0 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -400,6 +400,10 @@
 	Kind FileKind
 }
 
+func (id FileIdentity) String() string {
+	return fmt.Sprintf("%s%s%s", id.URI, id.Hash, id.Kind)
+}
+
 // FileKind describes the kind of the file in question.
 // It can be one of Go, mod, or sum.
 type FileKind int