internal/lsp: prepare for deletion of view.modURI

Splitting this CL out of CL 257417 to minimize the number of changes.
A few of the view's methods are moved to the snapshot, as they will
soon rely on the snapshot's modules field. Some dead code is also
deleted.

We now populate the snapshot's modules field even when
ExperimentalWorkspaceModule is not true, but we stop looking for modules
after searching the view's root.

Change-Id: Id0068ec10fafcfa6f7694dfcb8aaee8cb025078f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/257961
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: Heschi Kreinick <heschi@google.com>
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index c80cd4c..7467075 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -58,16 +58,6 @@
 			query = append(query, string(scope))
 		case fileURI:
 			query = append(query, fmt.Sprintf("file=%s", span.URI(scope).Filename()))
-		case directoryURI:
-			filename := span.URI(scope).Filename()
-			q := fmt.Sprintf("%s/...", filename)
-			// Simplify the query if it will be run in the requested directory.
-			// This ensures compatibility with Go 1.12 that doesn't allow
-			// <directory>/... in GOPATH mode.
-			if s.view.rootURI.Filename() == filename {
-				q = "./..."
-			}
-			query = append(query, q)
 		case moduleLoadScope:
 			query = append(query, fmt.Sprintf("%s/...", scope))
 		case viewLoadScope:
@@ -82,7 +72,7 @@
 			panic(fmt.Sprintf("unknown scope type %T", scope))
 		}
 		switch scope.(type) {
-		case directoryURI, viewLoadScope:
+		case viewLoadScope:
 			containsDir = true
 		}
 	}
diff --git a/internal/lsp/cache/pkg.go b/internal/lsp/cache/pkg.go
index faa0b29..3dde66c 100644
--- a/internal/lsp/cache/pkg.go
+++ b/internal/lsp/cache/pkg.go
@@ -42,7 +42,6 @@
 // Declare explicit types for files and directories to distinguish between the two.
 type (
 	fileURI         span.URI
-	directoryURI    span.URI
 	moduleLoadScope string
 	viewLoadScope   span.URI
 )
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 9122ab3..c1704e3 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -169,11 +169,9 @@
 	// If workspace module mode is enabled, find all of the modules in the
 	// workspace.
 	var modules map[span.URI]*moduleRoot
-	if options.ExperimentalWorkspaceModule {
-		modules, err = findWorkspaceModules(ctx, ws.rootURI, options)
-		if err != nil {
-			return nil, nil, func() {}, err
-		}
+	modules, err = findWorkspaceModules(ctx, ws.rootURI, options)
+	if err != nil {
+		return nil, nil, func() {}, err
 	}
 
 	// Now that we have set all required fields,
@@ -276,17 +274,14 @@
 			}
 		}
 		// We're only interested in go.mod files.
-		if filepath.Base(path) != "go.mod" {
-			return nil
+		if filepath.Base(path) == "go.mod" {
+			m := newModule(ctx, span.URIFromPath(path))
+			modules[m.rootURI] = m
 		}
-		// 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))
-		modules[rootURI] = &moduleRoot{
-			rootURI: rootURI,
-			modURI:  modURI,
-			sumURI:  span.URIFromPath(sumFilename(modURI)),
+		// If we are not using experimental workspace modules, don't continue
+		// the search past the view's root.
+		if !options.ExperimentalWorkspaceModule {
+			return filepath.SkipDir
 		}
 		return nil
 	})
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 5cc18bc..8a5fc01 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -597,28 +597,25 @@
 	return results, nil
 }
 
-func (s *snapshot) GoModForFile(ctx context.Context, fh source.FileHandle) (source.VersionedFileHandle, error) {
-	if fh.Kind() != source.Go {
-		return nil, fmt.Errorf("expected Go file, got %s", fh.Kind())
-	}
+func (s *snapshot) GoModForFile(ctx context.Context, uri span.URI) (span.URI, error) {
 	if len(s.modules) == 0 {
 		if s.view.modURI == "" {
-			return nil, errors.New("no modules in this view")
+			return "", errors.New("no modules in this view")
 		}
-		return s.GetFile(ctx, s.view.modURI)
+		return s.view.modURI, nil
 	}
 	var match span.URI
 	for _, m := range s.modules {
 		// Add an os.PathSeparator to the end of each directory to make sure
 		// that foo/apple/banana does not match foo/a.
-		if !strings.HasPrefix(fh.URI().Filename()+string(os.PathSeparator), m.rootURI.Filename()+string(os.PathSeparator)) {
+		if !strings.HasPrefix(uri.Filename()+string(os.PathSeparator), m.rootURI.Filename()+string(os.PathSeparator)) {
 			continue
 		}
 		if len(m.modURI) > len(match) {
 			match = m.modURI
 		}
 	}
-	return s.GetFile(ctx, match)
+	return match, nil
 }
 
 func (s *snapshot) getPackage(id packageID, mode source.ParseMode) *packageHandle {
@@ -1065,7 +1062,8 @@
 			rootURI := span.URIFromPath(filepath.Dir(withoutURI.Filename()))
 			if currentMod {
 				if _, ok := result.modules[rootURI]; !ok {
-					result.addModule(ctx, currentFH.URI())
+					m := newModule(ctx, currentFH.URI())
+					result.modules[m.rootURI] = m
 					result.view.definitelyReinitialize()
 				}
 			} else if originalMod {
@@ -1542,13 +1540,13 @@
 	return file, nil
 }
 
-func (s *snapshot) addModule(ctx context.Context, modURI span.URI) {
+func newModule(ctx context.Context, modURI span.URI) *moduleRoot {
 	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{
+	return &moduleRoot{
 		rootURI: rootURI,
 		modURI:  modURI,
 		sumURI:  sumURI,
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index abf6f4c..f718b66 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -209,15 +209,15 @@
 
 func (v *View) ID() string { return v.id }
 
-func (v *View) ValidBuildConfiguration() bool {
-	return v.hasValidBuildConfiguration
+func (s *snapshot) ValidBuildConfiguration() bool {
+	return s.view.hasValidBuildConfiguration
 }
 
-func (v *View) ModFiles() []span.URI {
-	if v.modURI == "" {
+func (s *snapshot) ModFiles() []span.URI {
+	if s.view.modURI == "" {
 		return nil
 	}
-	return []span.URI{v.modURI}
+	return []span.URI{s.view.modURI}
 }
 
 // tempModFile creates a temporary go.mod file based on the contents of the
@@ -340,13 +340,13 @@
 	return snapshot, release, nil
 }
 
-func (v *View) WriteEnv(ctx context.Context, w io.Writer) error {
-	v.optionsMu.Lock()
-	env, buildFlags := v.envLocked()
-	v.optionsMu.Unlock()
+func (s *snapshot) WriteEnv(ctx context.Context, w io.Writer) error {
+	s.view.optionsMu.Lock()
+	env, buildFlags := s.view.envLocked()
+	s.view.optionsMu.Unlock()
 
 	fullEnv := make(map[string]string)
-	for k, v := range v.goEnv {
+	for k, v := range s.view.goEnv {
 		fullEnv[k] = v
 	}
 	for _, v := range env {
@@ -360,7 +360,7 @@
 
 	}
 	fmt.Fprintf(w, "go env for %v\n(root %s)\n(valid build configuration = %v)\n(build flags: %v)\n",
-		v.folder.Filename(), v.rootURI.Filename(), v.hasValidBuildConfiguration, buildFlags)
+		s.view.folder.Filename(), s.view.rootURI.Filename(), s.view.hasValidBuildConfiguration, buildFlags)
 	for k, v := range fullEnv {
 		fmt.Fprintf(w, "%s=%s\n", k, v)
 	}
@@ -646,18 +646,18 @@
 	return v.backgroundCtx
 }
 
-func (v *View) IgnoredFile(uri span.URI) bool {
+func (s *snapshot) IgnoredFile(uri span.URI) bool {
 	filename := uri.Filename()
 	var prefixes []string
-	if v.modURI == "" {
-		for _, entry := range filepath.SplitList(v.gopath) {
+	if len(s.modules) == 0 {
+		for _, entry := range filepath.SplitList(s.view.gopath) {
 			prefixes = append(prefixes, filepath.Join(entry, "src"))
 		}
 	} else {
-		mainMod := filepath.Dir(v.modURI.Filename())
-		prefixes = []string{mainMod, v.gomodcache}
+		for _, m := range s.modules {
+			prefixes = []string{m.rootURI.Filename(), s.view.gomodcache}
+		}
 	}
-
 	for _, prefix := range prefixes {
 		if strings.HasPrefix(filename, prefix) {
 			return checkIgnored(filename[len(prefix):])
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index 1129fc1..3ae9694 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -459,11 +459,11 @@
 	case source.Mod:
 		modFH = fh
 	case source.Go:
-		var err error
-		modFH, err = snapshot.GoModForFile(ctx, fh)
+		modURI, err := snapshot.GoModForFile(ctx, fh.URI())
 		if err != nil {
 			return nil, err
 		}
+		modFH, err = snapshot.GetFile(ctx, modURI)
 	}
 	tidied, err := snapshot.ModTidy(ctx, modFH)
 	if err == source.ErrTmpModfileUnsupported {
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index 6b4fd7c..23f50ca 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -339,7 +339,7 @@
 	})
 }
 
-func (s *Server) runGoGenerate(ctx context.Context, snapshot source.Snapshot, uri span.URI, recursive bool, work *workDone) error {
+func (s *Server) runGoGenerate(ctx context.Context, snapshot source.Snapshot, dir span.URI, recursive bool, work *workDone) error {
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 
@@ -353,7 +353,7 @@
 
 	stderr := io.MultiWriter(er, workDoneWriter{work})
 
-	if err := snapshot.RunGoCommandPiped(ctx, uri.Filename(), "generate", args, er, stderr); err != nil {
+	if err := snapshot.RunGoCommandPiped(ctx, dir.Filename(), "generate", args, er, stderr); err != nil {
 		return err
 	}
 	return nil
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index b31de97..8cebae0 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -130,9 +130,11 @@
 		return nil, nil
 	}
 	var (
-		showMsg *protocol.ShowMessageParams
-		wg      sync.WaitGroup
+		showMsg     *protocol.ShowMessageParams
+		wg          sync.WaitGroup
+		seenFilesMu sync.Mutex
 	)
+	seenFiles := make(map[span.URI]struct{})
 	for _, pkg := range wsPkgs {
 		wg.Add(1)
 		go func(pkg source.Package) {
@@ -141,6 +143,10 @@
 			withAnalysis := alwaysAnalyze // only run analyses for packages with open files
 			var gcDetailsDir span.URI     // find the package's optimization details, if available
 			for _, pgf := range pkg.CompiledGoFiles() {
+				seenFilesMu.Lock()
+				seenFiles[pgf.URI] = struct{}{}
+				seenFilesMu.Unlock()
+
 				if snapshot.IsOpen(pgf.URI) {
 					withAnalysis = true
 				}
@@ -159,7 +165,7 @@
 
 			// Check if might want to warn the user about their build configuration.
 			// Our caller decides whether to send the message.
-			if warn && !snapshot.View().ValidBuildConfiguration() {
+			if warn && !snapshot.ValidBuildConfiguration() {
 				showMsg = &protocol.ShowMessageParams{
 					Type:    protocol.Warning,
 					Message: `You are neither in a module nor in your GOPATH. If you are using modules, please open your editor to a directory in your module. If you believe this warning is incorrect, please file an issue: https://github.com/golang/go/issues/new.`,
@@ -187,10 +193,18 @@
 			}
 		}(pkg)
 	}
+	wg.Wait()
 	// Confirm that every opened file belongs to a package (if any exist in
 	// the workspace). Otherwise, add a diagnostic to the file.
 	if len(wsPkgs) > 0 {
 		for _, o := range s.session.Overlays() {
+			seenFilesMu.Lock()
+			_, ok := seenFiles[o.URI()]
+			seenFilesMu.Unlock()
+			if ok {
+				continue
+			}
+
 			diagnostic := s.checkForOrphanedFile(ctx, snapshot, o.URI())
 			if diagnostic == nil {
 				continue
@@ -200,7 +214,6 @@
 			addReport(o.VersionedFileIdentity(), true, []*source.Diagnostic{diagnostic})
 		}
 	}
-	wg.Wait()
 	return reports, showMsg
 }
 
@@ -405,7 +418,7 @@
 	}
 
 	// All other workarounds are for errors associated with modules.
-	if len(snapshot.View().ModFiles()) == 0 {
+	if len(snapshot.ModFiles()) == 0 {
 		return false
 	}
 
@@ -431,7 +444,7 @@
 		// to a specific module, so this will re-run `go mod vendor` in every
 		// known module with a vendor directory.
 		// TODO(rstambler): Only re-run `go mod vendor` in the relevant module.
-		for _, uri := range snapshot.View().ModFiles() {
+		for _, uri := range snapshot.ModFiles() {
 			// Check that there is a vendor directory in the module before
 			// running `go mod vendor`.
 			if info, _ := os.Stat(filepath.Join(filepath.Dir(uri.Filename()), "vendor")); info == nil {
@@ -459,7 +472,7 @@
 	if errors.Is(loadErr, source.PackagesLoadError) {
 		// TODO(rstambler): Construct the diagnostics in internal/lsp/cache
 		// so that we can avoid this here.
-		for _, uri := range snapshot.View().ModFiles() {
+		for _, uri := range snapshot.ModFiles() {
 			fh, err := snapshot.GetFile(ctx, uri)
 			if err != nil {
 				return false
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index 78b8e0a..0ca97c9 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -219,7 +219,7 @@
 
 		// Print each view's environment.
 		buf := &bytes.Buffer{}
-		if err := view.WriteEnv(ctx, buf); err != nil {
+		if err := snapshot.WriteEnv(ctx, buf); err != nil {
 			event.Error(ctx, "failed to write environment", err, tag.Directory.Of(view.Folder().Filename()))
 			continue
 		}
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index a87f0cd..4541b30 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -49,8 +49,7 @@
 	tests.DefaultOptions(options)
 	session.SetOptions(options)
 	options.Env = datum.Config.Env
-	view, _, release, err := session.NewView(ctx, datum.Config.Dir, span.URIFromPath(datum.Config.Dir), options)
-	release()
+	view, snapshot, release, err := session.NewView(ctx, datum.Config.Dir, span.URIFromPath(datum.Config.Dir), options)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -63,7 +62,8 @@
 	view.SetOptions(ctx, options)
 
 	// Only run the -modfile specific tests in module mode with Go 1.14 or above.
-	datum.ModfileFlagAvailable = len(view.ModFiles()) > 0 && testenv.Go1Point() >= 14
+	datum.ModfileFlagAvailable = len(snapshot.ModFiles()) > 0 && testenv.Go1Point() >= 14
+	release()
 
 	var modifications []source.FileModification
 	for filename, content := range datum.Config.Overlay {
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index d0ce490..d4d3cad 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -27,7 +27,7 @@
 	defer done()
 
 	reports := map[source.VersionedFileIdentity][]*source.Diagnostic{}
-	for _, uri := range snapshot.View().ModFiles() {
+	for _, uri := range snapshot.ModFiles() {
 		fh, err := snapshot.GetFile(ctx, uri)
 		if err != nil {
 			return nil, err
diff --git a/internal/lsp/mod/hover.go b/internal/lsp/mod/hover.go
index 1272e0b..739ebef 100644
--- a/internal/lsp/mod/hover.go
+++ b/internal/lsp/mod/hover.go
@@ -17,7 +17,7 @@
 
 func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
 	var found bool
-	for _, uri := range snapshot.View().ModFiles() {
+	for _, uri := range snapshot.ModFiles() {
 		if fh.URI() == uri {
 			found = true
 			break
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index 5558d7c..5388862 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -41,7 +41,7 @@
 func Diagnostics(ctx context.Context, snapshot Snapshot, pkg Package, withAnalysis bool) (map[VersionedFileIdentity][]*Diagnostic, bool, error) {
 	onlyIgnoredFiles := true
 	for _, pgf := range pkg.CompiledGoFiles() {
-		onlyIgnoredFiles = onlyIgnoredFiles && snapshot.View().IgnoredFile(pgf.URI)
+		onlyIgnoredFiles = onlyIgnoredFiles && snapshot.IgnoredFile(pgf.URI)
 	}
 	if onlyIgnoredFiles {
 		return nil, false, nil
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index f098921..f40a9b8 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -32,6 +32,14 @@
 	// Fileset returns the Fileset used to parse all the Go files in this snapshot.
 	FileSet() *token.FileSet
 
+	// ValidBuildConfiguration returns true if there is some error in the
+	// user's workspace. In particular, if they are both outside of a module
+	// and their GOPATH.
+	ValidBuildConfiguration() bool
+
+	// WriteEnv writes the view-specific environment to the io.Writer.
+	WriteEnv(ctx context.Context, w io.Writer) error
+
 	// FindFile returns the FileHandle for the given URI, if it is already
 	// in the given snapshot.
 	FindFile(uri span.URI) VersionedFileHandle
@@ -49,6 +57,10 @@
 	// IsSaved returns whether the contents are saved on disk or not.
 	IsSaved(uri span.URI) bool
 
+	// IgnoredFile reports if a file would be ignored by a `go list` of the whole
+	// workspace.
+	IgnoredFile(uri span.URI) bool
+
 	// ParseGo returns the parsed AST for the file.
 	// If the file is not available, returns nil and an error.
 	ParseGo(ctx context.Context, fh FileHandle, mode ParseMode) (*ParsedGoFile, error)
@@ -78,6 +90,10 @@
 	// snapshot's root folder will be used as the working directory.
 	RunGoCommandDirect(ctx context.Context, wd, verb string, args []string) error
 
+	// ModFiles are the go.mod files enclosed in the snapshot's view and known
+	// to the snapshot.
+	ModFiles() []span.URI
+
 	// ParseMod is used to parse go.mod files.
 	ParseMod(ctx context.Context, fh FileHandle) (*ParsedModule, error)
 
@@ -93,8 +109,8 @@
 	// the given go.mod file.
 	ModTidy(ctx context.Context, fh FileHandle) (*TidiedModule, error)
 
-	// GoModForFile returns the go.mod file handle for the given Go file.
-	GoModForFile(ctx context.Context, fh FileHandle) (VersionedFileHandle, error)
+	// GoModForFile returns the URI of the go.mod file for the given URI.
+	GoModForFile(ctx context.Context, uri span.URI) (span.URI, error)
 
 	// BuildWorkspaceModFile builds the contents of mod file to be used for
 	// multi-module workspace.
@@ -162,9 +178,6 @@
 	// Folder returns the folder with which this view was created.
 	Folder() span.URI
 
-	// ModFiles is the go.mod file at the root of this view. It may not exist.
-	ModFiles() []span.URI
-
 	// BackgroundContext returns a context used for all background processing
 	// on behalf of this view.
 	BackgroundContext() context.Context
@@ -172,9 +185,6 @@
 	// Shutdown closes this view, and detaches it from its session.
 	Shutdown(ctx context.Context)
 
-	// WriteEnv writes the view-specific environment to the io.Writer.
-	WriteEnv(ctx context.Context, w io.Writer) error
-
 	// RunProcessEnvFunc runs fn with the process env for this snapshot's view.
 	// Note: the process env contains cached module and filesystem state.
 	RunProcessEnvFunc(ctx context.Context, fn func(*imports.Options) error) error
@@ -194,18 +204,9 @@
 	// Rebuild rebuilds the current view, replacing the original view in its session.
 	Rebuild(ctx context.Context) (Snapshot, func(), error)
 
-	// InvalidBuildConfiguration returns true if there is some error in the
-	// user's workspace. In particular, if they are both outside of a module
-	// and their GOPATH.
-	ValidBuildConfiguration() bool
-
 	// IsGoPrivatePath reports whether target is a private import path, as identified
 	// by the GOPRIVATE environment variable.
 	IsGoPrivatePath(path string) bool
-
-	// IgnoredFile reports if a file would be ignored by a `go list` of the whole
-	// workspace.
-	IgnoredFile(uri span.URI) bool
 }
 
 type BuiltinPackage struct {
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index 4606e25..89aea44 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -254,7 +254,7 @@
 		// If a modification comes in for the view's go.mod file and the view
 		// was never properly initialized, or the view does not have
 		// a go.mod file, try to recreate the associated view.
-		if len(snapshot.View().ModFiles()) == 0 {
+		if len(snapshot.ModFiles()) == 0 {
 			for _, uri := range uris {
 				// Don't rebuild the view until the go.mod is on disk.
 				if !snapshot.IsSaved(uri) {