internal/lsp: track all go.mod changes no matter the workspace mode

In some cases (such as presenting more helpful error messages), we need
to know the number of go.mod files in the workspace--not just the
active go.mod files. Track both known and active mod files separately.

Change-Id: I068f76c2930c90cd0fdf5ce637e5934210880f65
Reviewed-on: https://go-review.googlesource.com/c/tools/+/273047
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/internal/lsp/cache/imports.go b/internal/lsp/cache/imports.go
index 524d581..cbad73e 100644
--- a/internal/lsp/cache/imports.go
+++ b/internal/lsp/cache/imports.go
@@ -36,7 +36,7 @@
 	// the mod file shouldn't be changing while people are autocompleting.
 	var modFileHash string
 	if snapshot.workspaceMode()&usesWorkspaceModule == 0 {
-		for m := range snapshot.workspace.activeModFiles() { // range to access the only element
+		for m := range snapshot.workspace.getActiveModFiles() { // range to access the only element
 			modFH, err := snapshot.GetFile(ctx, m)
 			if err != nil {
 				return err
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 41c537f..0697669 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -131,14 +131,14 @@
 
 func (s *snapshot) ModFiles() []span.URI {
 	var uris []span.URI
-	for modURI := range s.workspace.activeModFiles() {
+	for modURI := range s.workspace.getActiveModFiles() {
 		uris = append(uris, modURI)
 	}
 	return uris
 }
 
 func (s *snapshot) ValidBuildConfiguration() bool {
-	return validBuildConfiguration(s.view.rootURI, &s.view.workspaceInformation, s.workspace.activeModFiles())
+	return validBuildConfiguration(s.view.rootURI, &s.view.workspaceInformation, s.workspace.getActiveModFiles())
 }
 
 // workspaceMode describes the way in which the snapshot's workspace should
@@ -155,7 +155,7 @@
 	// 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 len(s.workspace.activeModFiles()) == 0 && validBuildConfiguration {
+	if len(s.workspace.getActiveModFiles()) == 0 && validBuildConfiguration {
 		return mode
 	}
 	mode |= moduleMode
@@ -262,7 +262,7 @@
 		// the passed-in working dir.
 		if mode == source.LoadWorkspace {
 			if s.workspaceMode()&usesWorkspaceModule == 0 {
-				for m := range s.workspace.activeModFiles() { // range to access the only element
+				for m := range s.workspace.getActiveModFiles() { // range to access the only element
 					modURI = m
 				}
 			} else {
@@ -762,7 +762,7 @@
 
 func (s *snapshot) GoModForFile(ctx context.Context, uri span.URI) span.URI {
 	var match span.URI
-	for modURI := range s.workspace.activeModFiles() {
+	for modURI := range s.workspace.getActiveModFiles() {
 		if !source.InDir(dirURI(modURI).Filename(), uri.Filename()) {
 			continue
 		}
@@ -1234,7 +1234,7 @@
 			// If the view's go.mod file's contents have changed, invalidate
 			// the metadata for every known package in the snapshot.
 			delete(result.parseModHandles, uri)
-			if _, ok := result.workspace.activeModFiles()[uri]; ok {
+			if _, ok := result.workspace.getActiveModFiles()[uri]; ok {
 				modulesChanged = true
 			}
 		}
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 505390c..11d685b 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -448,13 +448,13 @@
 func (s *snapshot) IgnoredFile(uri span.URI) bool {
 	filename := uri.Filename()
 	var prefixes []string
-	if len(s.workspace.activeModFiles()) == 0 {
+	if len(s.workspace.getActiveModFiles()) == 0 {
 		for _, entry := range filepath.SplitList(s.view.gopath) {
 			prefixes = append(prefixes, filepath.Join(entry, "src"))
 		}
 	} else {
 		prefixes = append(prefixes, s.view.gomodcache)
-		for m := range s.workspace.activeModFiles() {
+		for m := range s.workspace.getActiveModFiles() {
 			prefixes = append(prefixes, dirURI(m).Filename())
 		}
 	}
@@ -524,7 +524,7 @@
 				Message:  err.Error(),
 			})
 		}
-		for modURI := range s.workspace.activeModFiles() {
+		for modURI := range s.workspace.getActiveModFiles() {
 			fh, err := s.GetFile(ctx, modURI)
 			if err != nil {
 				addError(modURI, err)
diff --git a/internal/lsp/cache/workspace.go b/internal/lsp/cache/workspace.go
index 30f87e4..709f6ce 100644
--- a/internal/lsp/cache/workspace.go
+++ b/internal/lsp/cache/workspace.go
@@ -56,8 +56,12 @@
 	root         span.URI
 	moduleSource workspaceSource
 
-	// modFiles holds the active go.mod files.
-	modFiles map[span.URI]struct{}
+	// activeModFiles holds the active go.mod files.
+	activeModFiles map[span.URI]struct{}
+
+	// knownModFiles holds the set of all go.mod files in the workspace.
+	// In all modes except for legacy, this is equivalent to modFiles.
+	knownModFiles map[span.URI]struct{}
 
 	// go111moduleOff indicates whether GO111MODULE=off has been configured in
 	// the environment.
@@ -77,54 +81,70 @@
 }
 
 func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, go111module string, experimental bool) (*workspace, error) {
+	// In experimental mode, the user may have a gopls.mod file that defines
+	// their workspace.
+	if experimental {
+		goplsModFH, err := fs.GetFile(ctx, goplsModURI(root))
+		if err != nil {
+			return nil, err
+		}
+		contents, err := goplsModFH.Read()
+		if err == nil {
+			file, activeModFiles, err := parseGoplsMod(root, goplsModFH.URI(), contents)
+			if err != nil {
+				return nil, err
+			}
+			return &workspace{
+				root:           root,
+				activeModFiles: activeModFiles,
+				knownModFiles:  activeModFiles,
+				file:           file,
+				moduleSource:   goplsModWorkspace,
+			}, nil
+		}
+	}
+	// Otherwise, in all other modes, search for all of the go.mod files in the
+	// workspace.
+	knownModFiles, err := findModules(ctx, root, 0)
+	if err != nil {
+		return nil, err
+	}
+	// When GO111MODULE=off, there are no active go.mod files.
 	if go111module == "off" {
 		return &workspace{
 			root:           root,
 			moduleSource:   legacyWorkspace,
+			knownModFiles:  knownModFiles,
 			go111moduleOff: true,
 		}, nil
 	}
+	// In legacy mode, not all known go.mod files will be considered active.
 	if !experimental {
-		modFiles, err := getLegacyModules(ctx, root, fs)
+		activeModFiles, err := getLegacyModules(ctx, root, fs)
 		if err != nil {
 			return nil, err
 		}
 		return &workspace{
-			root:         root,
-			modFiles:     modFiles,
-			moduleSource: legacyWorkspace,
+			root:           root,
+			activeModFiles: activeModFiles,
+			knownModFiles:  knownModFiles,
+			moduleSource:   legacyWorkspace,
 		}, nil
 	}
-	goplsModFH, err := fs.GetFile(ctx, goplsModURI(root))
-	if err != nil {
-		return nil, err
-	}
-	contents, err := goplsModFH.Read()
-	if err == nil {
-		file, modFiles, err := parseGoplsMod(root, goplsModFH.URI(), contents)
-		if err != nil {
-			return nil, err
-		}
-		return &workspace{
-			root:         root,
-			modFiles:     modFiles,
-			file:         file,
-			moduleSource: goplsModWorkspace,
-		}, nil
-	}
-	modFiles, err := findModules(ctx, root, 0)
-	if err != nil {
-		return nil, err
-	}
 	return &workspace{
-		root:         root,
-		modFiles:     modFiles,
-		moduleSource: fileSystemWorkspace,
+		root:           root,
+		activeModFiles: knownModFiles,
+		knownModFiles:  knownModFiles,
+		moduleSource:   fileSystemWorkspace,
 	}, nil
 }
 
-func (wm *workspace) activeModFiles() map[span.URI]struct{} {
-	return wm.modFiles
+func (wm *workspace) getKnownModFiles() map[span.URI]struct{} {
+	return wm.knownModFiles
+}
+
+func (wm *workspace) getActiveModFiles() map[span.URI]struct{} {
+	return wm.activeModFiles
 }
 
 // modFile gets the workspace modfile associated with this workspace,
@@ -154,7 +174,7 @@
 	// If our module source is not gopls.mod, try to build the workspace module
 	// from modules. Fall back on the pre-existing mod file if parsing fails.
 	if wm.moduleSource != goplsModWorkspace {
-		file, err := buildWorkspaceModFile(ctx, wm.modFiles, fs)
+		file, err := buildWorkspaceModFile(ctx, wm.activeModFiles, fs)
 		switch {
 		case err == nil:
 			wm.file = file
@@ -209,15 +229,16 @@
 	// built.
 	wm.buildMu.Lock()
 	defer wm.buildMu.Unlock()
+
 	// Any gopls.mod change is processed first, followed by go.mod changes, as
 	// changes to gopls.mod may affect the set of active go.mod files.
 	var (
-		// New values. We return a new workspace module if and only if modFiles is
-		// non-nil.
-		modFiles     map[span.URI]struct{}
-		moduleSource = wm.moduleSource
-		modFile      = wm.file
-		err          error
+		// New values. We return a new workspace module if and only if
+		// knownModFiles is non-nil.
+		knownModFiles map[span.URI]struct{}
+		moduleSource  = wm.moduleSource
+		modFile       = wm.file
+		err           error
 	)
 	if wm.moduleSource == goplsModWorkspace {
 		// If we are currently reading the modfile from gopls.mod, we default to
@@ -237,7 +258,7 @@
 				if err == nil {
 					modFile = parsedFile
 					moduleSource = goplsModWorkspace
-					modFiles = parsedModules
+					knownModFiles = parsedModules
 				} else {
 					// Note that modFile is not invalidated here.
 					event.Error(ctx, "parsing gopls.mod", err)
@@ -245,7 +266,7 @@
 			} else {
 				// gopls.mod is deleted. search for modules again.
 				moduleSource = fileSystemWorkspace
-				modFiles, err = findModules(ctx, wm.root, 0)
+				knownModFiles, err = findModules(ctx, wm.root, 0)
 				// the modFile is no longer valid.
 				if err != nil {
 					event.Error(ctx, "finding file system modules", err)
@@ -265,39 +286,47 @@
 			if !isGoMod(uri) {
 				continue
 			}
-			if wm.moduleSource == legacyWorkspace && source.CompareURI(modURI(wm.root), uri) != 0 {
-				// Legacy mode only considers a module a workspace root.
-				continue
-			}
 			if !source.InDir(wm.root.Filename(), uri.Filename()) {
 				// Otherwise, the module must be contained within the workspace root.
 				continue
 			}
-			if modFiles == nil {
-				modFiles = make(map[span.URI]struct{})
-				for k := range wm.modFiles {
-					modFiles[k] = struct{}{}
+			if knownModFiles == nil {
+				knownModFiles = make(map[span.URI]struct{})
+				for k := range wm.knownModFiles {
+					knownModFiles[k] = struct{}{}
 				}
 			}
 			if change.exists {
-				modFiles[uri] = struct{}{}
+				knownModFiles[uri] = struct{}{}
 			} else {
-				delete(modFiles, uri)
+				delete(knownModFiles, uri)
 			}
 		}
 	}
-	if modFiles != nil {
-		// If GO111MODULE=off, don't update the set of active go.mod files.
+	if knownModFiles != nil {
+		var activeModFiles map[span.URI]struct{}
 		if wm.go111moduleOff {
-			modFiles = wm.modFiles
+			// If GO111MODULE=off, the set of active go.mod files is unchanged.
+			activeModFiles = wm.activeModFiles
+		} else {
+			activeModFiles = make(map[span.URI]struct{})
+			for uri := range knownModFiles {
+				// Legacy mode only considers a module a workspace root, so don't
+				// update the active go.mod files map.
+				if wm.moduleSource == legacyWorkspace && source.CompareURI(modURI(wm.root), uri) != 0 {
+					continue
+				}
+				activeModFiles[uri] = struct{}{}
+			}
 		}
 		// Any change to modules triggers a new version.
 		return &workspace{
-			root:         wm.root,
-			moduleSource: moduleSource,
-			modFiles:     modFiles,
-			file:         modFile,
-			wsDirs:       wm.wsDirs,
+			root:           wm.root,
+			moduleSource:   moduleSource,
+			activeModFiles: activeModFiles,
+			knownModFiles:  knownModFiles,
+			file:           modFile,
+			wsDirs:         wm.wsDirs,
 		}, true
 	}
 	// No change. Just return wm, since it is immutable.
diff --git a/internal/lsp/cache/workspace_test.go b/internal/lsp/cache/workspace_test.go
index 58be13a..3f1d8a9 100644
--- a/internal/lsp/cache/workspace_test.go
+++ b/internal/lsp/cache/workspace_test.go
@@ -221,7 +221,7 @@
 		t.Errorf("module source = %v, want %v", got.moduleSource, wantSource)
 	}
 	modules := make(map[span.URI]struct{})
-	for k := range got.activeModFiles() {
+	for k := range got.getActiveModFiles() {
 		modules[k] = struct{}{}
 	}
 	for _, modPath := range want {