internal/lsp: stop using modURI as much as possible

This change switches over load and RunProcessEnvFunc to use the
snapshot's modules instead of the view's modURI. These do not seem to
have been the racy parts of CL 257417.

Change-Id: I317a350fc4b0c62d77858455a0e2e61148804ecd
Reviewed-on: https://go-review.googlesource.com/c/tools/+/257969
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/gopls/internal/regtest/workspace_test.go b/gopls/internal/regtest/workspace_test.go
index 3f79d57..43d4ec6 100644
--- a/gopls/internal/regtest/workspace_test.go
+++ b/gopls/internal/regtest/workspace_test.go
@@ -257,8 +257,8 @@
 	})
 }
 
-// This change tests that the version of the module used changes after it has
-// been added to the workspace.
+// 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 --
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index 7467075..63254f0 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -82,24 +82,11 @@
 	defer done()
 
 	cleanup := func() {}
-
-	var modFH, sumFH source.FileHandle
-	var err error
-	if s.view.modURI != "" {
-		modFH, err = s.GetFile(ctx, s.view.modURI)
-		if err != nil {
-			return err
-		}
-	}
-	if s.view.sumURI != "" {
-		sumFH, err = s.GetFile(ctx, s.view.sumURI)
-		if err != nil {
-			return err
-		}
-	}
-
 	wdir := s.view.rootURI.Filename()
+
 	var buildFlags []string
+	var modURI span.URI
+	var modContent []byte
 	switch {
 	case s.view.workspaceMode&usesWorkspaceModule != 0:
 		var (
@@ -111,7 +98,33 @@
 			return err
 		}
 		wdir = tmpDir.Filename()
+		modURI = span.URIFromPath(filepath.Join(wdir, "go.mod"))
+		modContent, err = ioutil.ReadFile(modURI.Filename())
+		if err != nil {
+			return err
+		}
 	case s.view.workspaceMode&tempModfile != 0:
+		// -modfile is unsupported when there are > 1 modules in the workspace.
+		if len(s.modules) != 1 {
+			panic(fmt.Sprintf("unsupported use of -modfile, expected 1 module, got %v", len(s.modules)))
+		}
+		var mod *moduleRoot
+		for _, m := range s.modules { // range to access the only element
+			mod = m
+		}
+		modURI = mod.modURI
+		modFH, err := s.GetFile(ctx, mod.modURI)
+		if err != nil {
+			return err
+		}
+		modContent, err = modFH.Read()
+		var sumFH source.FileHandle
+		if mod.sumURI != "" {
+			sumFH, err = s.GetFile(ctx, mod.sumURI)
+			if err != nil {
+				return err
+			}
+		}
 		var tmpURI span.URI
 		tmpURI, cleanup, err = tempModFile(modFH, sumFH)
 		if err != nil {
@@ -123,7 +136,7 @@
 	cfg := s.config(ctx, wdir)
 	cfg.BuildFlags = append(cfg.BuildFlags, buildFlags...)
 
-	modMod, err := s.view.needsModEqualsMod(ctx, modFH)
+	modMod, err := s.view.needsModEqualsMod(ctx, modURI, modContent)
 	if err != nil {
 		return err
 	}
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 457b422..e9aa1dd 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -167,7 +167,7 @@
 	}
 
 	// If workspace module mode is enabled, find all of the modules in the
-	// workspace.
+	// workspace. By default, we just find the root module.
 	var modules map[span.URI]*moduleRoot
 	modules, err = findWorkspaceModules(ctx, ws.rootURI, options)
 	if err != nil {
@@ -254,6 +254,8 @@
 //
 // 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.
+//
+// TODO(rstambler): Check overlays for go.mod files.
 func findWorkspaceModules(ctx context.Context, root span.URI, options *source.Options) (map[span.URI]*moduleRoot, error) {
 	// Walk the view's folder to find all modules in the view.
 	modules := make(map[span.URI]*moduleRoot)
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 8a5fc01..01e7856 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -201,11 +201,14 @@
 	return runner.RunPiped(ctx, *inv, stdout, stderr)
 }
 
-// Assumes that modURI is only provided when the -modfile flag is enabled.
 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
+	modURI := s.GoModForFile(ctx, span.URIFromPath(cfg.Dir))
 	if allowTempModfile && s.view.workspaceMode&tempModfile != 0 {
-		modFH, err := s.GetFile(ctx, s.view.modURI)
+		if modURI == "" {
+			return "", nil, nil, cleanup, fmt.Errorf("no go.mod file found in %s", cfg.Dir)
+		}
+		modFH, err := s.GetFile(ctx, modURI)
 		if err != nil {
 			return "", nil, nil, cleanup, err
 		}
@@ -219,14 +222,18 @@
 		cfg.BuildFlags = append(cfg.BuildFlags, fmt.Sprintf("-modfile=%s", tmpURI.Filename()))
 	}
 	if verb != "mod" && verb != "get" {
-		var modFH source.FileHandle
-		if s.view.modURI != "" {
-			modFH, err = s.GetFile(ctx, s.view.modURI)
+		var modContent []byte
+		if modURI != "" {
+			modFH, err := s.GetFile(ctx, modURI)
 			if err != nil {
 				return "", nil, nil, cleanup, err
 			}
+			modContent, err = modFH.Read()
+			if err != nil {
+				return "", nil, nil, nil, err
+			}
 		}
-		modMod, err := s.view.needsModEqualsMod(ctx, modFH)
+		modMod, err := s.view.needsModEqualsMod(ctx, modURI, modContent)
 		if err != nil {
 			return "", nil, nil, cleanup, err
 		}
@@ -597,25 +604,17 @@
 	return results, nil
 }
 
-func (s *snapshot) GoModForFile(ctx context.Context, uri span.URI) (span.URI, error) {
-	if len(s.modules) == 0 {
-		if s.view.modURI == "" {
-			return "", errors.New("no modules in this view")
-		}
-		return s.view.modURI, nil
-	}
+func (s *snapshot) GoModForFile(ctx context.Context, uri span.URI) span.URI {
 	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(uri.Filename()+string(os.PathSeparator), m.rootURI.Filename()+string(os.PathSeparator)) {
+		if !isSubdirectory(m.rootURI.Filename(), uri.Filename()) {
 			continue
 		}
 		if len(m.modURI) > len(match) {
 			match = m.modURI
 		}
 	}
-	return match, nil
+	return match
 }
 
 func (s *snapshot) getPackage(id packageID, mode source.ParseMode) *packageHandle {
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index f718b66..e7c871d 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -357,7 +357,6 @@
 		if _, ok := fullEnv[s[0]]; ok {
 			fullEnv[s[0]] = s[1]
 		}
-
 	}
 	fmt.Fprintf(w, "go env for %v\n(root %s)\n(valid build configuration = %v)\n(build flags: %v)\n",
 		s.view.folder.Filename(), s.view.rootURI.Filename(), s.view.hasValidBuildConfiguration, buildFlags)
@@ -367,9 +366,9 @@
 	return nil
 }
 
-func (v *View) RunProcessEnvFunc(ctx context.Context, fn func(*imports.Options) error) error {
-	v.importsMu.Lock()
-	defer v.importsMu.Unlock()
+func (s *snapshot) RunProcessEnvFunc(ctx context.Context, fn func(*imports.Options) error) error {
+	s.view.importsMu.Lock()
+	defer s.view.importsMu.Unlock()
 
 	// Use temporary go.mod files, but always go to disk for the contents.
 	// Rebuilding the cache is expensive, and we don't want to do it for
@@ -377,28 +376,41 @@
 	var modFH, sumFH source.FileHandle
 	var modFileIdentifier string
 	var err error
-	if v.modURI != "" {
-		modFH, err = v.session.cache.getFile(ctx, v.modURI)
+	// TODO(heschik): Change the goimports logic to use a persistent workspace
+	// module for workspace module mode.
+	//
+	// Get the go.mod file that corresponds to this view's root URI. This is
+	// broken because it assumes that the view's root is a module, but this is
+	// not more broken than the previous state--it is a temporary hack that
+	// should be removed ASAP.
+	var match *moduleRoot
+	for _, m := range s.modules {
+		if m.rootURI == s.view.rootURI {
+			match = m
+		}
+	}
+	if match != nil {
+		modFH, err = s.GetFile(ctx, match.modURI)
 		if err != nil {
 			return err
 		}
 		modFileIdentifier = modFH.FileIdentity().Hash
-	}
-	if v.sumURI != "" {
-		sumFH, err = v.session.cache.getFile(ctx, v.sumURI)
-		if err != nil {
-			return err
+		if match.sumURI != "" {
+			sumFH, err = s.GetFile(ctx, match.sumURI)
+			if err != nil {
+				return err
+			}
 		}
 	}
 	// v.goEnv is immutable -- changes make a new view. Options can change.
 	// We can't compare build flags directly because we may add -modfile.
-	v.optionsMu.Lock()
-	localPrefix := v.options.Local
-	currentBuildFlags := v.options.BuildFlags
-	changed := !reflect.DeepEqual(currentBuildFlags, v.cachedBuildFlags) ||
-		v.options.VerboseOutput != (v.processEnv.Logf != nil) ||
-		modFileIdentifier != v.cachedModFileIdentifier
-	v.optionsMu.Unlock()
+	s.view.optionsMu.Lock()
+	localPrefix := s.view.options.Local
+	currentBuildFlags := s.view.options.BuildFlags
+	changed := !reflect.DeepEqual(currentBuildFlags, s.view.cachedBuildFlags) ||
+		s.view.options.VerboseOutput != (s.view.processEnv.Logf != nil) ||
+		modFileIdentifier != s.view.cachedModFileIdentifier
+	s.view.optionsMu.Unlock()
 
 	// If anything relevant to imports has changed, clear caches and
 	// update the processEnv. Clearing caches blocks on any background
@@ -407,15 +419,15 @@
 		// As a special case, skip cleanup the first time -- we haven't fully
 		// initialized the environment yet and calling GetResolver will do
 		// unnecessary work and potentially mess up the go.mod file.
-		if v.cleanupProcessEnv != nil {
-			if resolver, err := v.processEnv.GetResolver(); err == nil {
+		if s.view.cleanupProcessEnv != nil {
+			if resolver, err := s.view.processEnv.GetResolver(); err == nil {
 				resolver.(*imports.ModuleResolver).ClearForNewMod()
 			}
-			v.cleanupProcessEnv()
+			s.view.cleanupProcessEnv()
 		}
-		v.cachedModFileIdentifier = modFileIdentifier
-		v.cachedBuildFlags = currentBuildFlags
-		v.cleanupProcessEnv, err = v.populateProcessEnv(ctx, modFH, sumFH)
+		s.view.cachedModFileIdentifier = modFileIdentifier
+		s.view.cachedBuildFlags = currentBuildFlags
+		s.view.cleanupProcessEnv, err = s.view.populateProcessEnv(ctx, modFH, sumFH)
 		if err != nil {
 			return err
 		}
@@ -430,7 +442,7 @@
 		FormatOnly:  false,
 		TabIndent:   true,
 		TabWidth:    8,
-		Env:         v.processEnv,
+		Env:         s.view.processEnv,
 		LocalPrefix: localPrefix,
 	}
 
@@ -438,14 +450,14 @@
 		return err
 	}
 
-	if v.cacheRefreshTimer == nil {
+	if s.view.cacheRefreshTimer == nil {
 		// Don't refresh more than twice per minute.
 		delay := 30 * time.Second
 		// Don't spend more than a couple percent of the time refreshing.
-		if adaptive := 50 * v.cacheRefreshDuration; adaptive > delay {
+		if adaptive := 50 * s.view.cacheRefreshDuration; adaptive > delay {
 			delay = adaptive
 		}
-		v.cacheRefreshTimer = time.AfterFunc(delay, v.refreshProcessEnv)
+		s.view.cacheRefreshTimer = time.AfterFunc(delay, s.view.refreshProcessEnv)
 	}
 
 	return nil
@@ -497,7 +509,16 @@
 	}
 	pe.Env["GO111MODULE"] = v.go111module
 
-	modmod, err := v.needsModEqualsMod(ctx, modFH)
+	var modURI span.URI
+	var modContent []byte
+	if modFH != nil {
+		modURI = modFH.URI()
+		modContent, err = modFH.Read()
+		if err != nil {
+			return nil, err
+		}
+	}
+	modmod, err := v.needsModEqualsMod(ctx, modURI, modContent)
 	if err != nil {
 		return cleanup, err
 	}
@@ -654,8 +675,9 @@
 			prefixes = append(prefixes, filepath.Join(entry, "src"))
 		}
 	} else {
+		prefixes = append(prefixes, s.view.gomodcache)
 		for _, m := range s.modules {
-			prefixes = []string{m.rootURI.Filename(), s.view.gomodcache}
+			prefixes = append(prefixes, m.rootURI.Filename())
 		}
 	}
 	for _, prefix := range prefixes {
@@ -898,9 +920,6 @@
 	}
 	// Check if the user is working within a module or if we have found
 	// multiple modules in the workspace.
-	if ws.modURI != "" {
-		return true
-	}
 	if len(modules) > 0 {
 		return true
 	}
@@ -1011,7 +1030,11 @@
 
 var modFlagRegexp = regexp.MustCompile(`-mod[ =](\w+)`)
 
-func (v *View) needsModEqualsMod(ctx context.Context, modFH source.FileHandle) (bool, error) {
+// TODO(rstambler): Consolidate modURI and modContent back into a FileHandle
+// after we have a version of the workspace go.mod file on disk. Getting a
+// FileHandle from the cache for temporary files is problematic, since we
+// cannot delete it.
+func (v *View) needsModEqualsMod(ctx context.Context, modURI span.URI, modContent []byte) (bool, error) {
 	if v.goversion < 16 || v.workspaceMode&moduleMode == 0 {
 		return false, nil
 	}
@@ -1028,21 +1051,11 @@
 		return modFlag == "vendor", nil
 	}
 
-	// In workspace module mode, there may not be a go.mod file.
-	// TODO: Once vendor mode is designed, update to check if it's on, however that works.
-	if modFH == nil {
-		return true, nil
-	}
-
-	modBytes, err := modFH.Read()
+	modFile, err := modfile.Parse(modURI.Filename(), modContent, nil)
 	if err != nil {
 		return false, err
 	}
-	modFile, err := modfile.Parse(modFH.URI().Filename(), modBytes, nil)
-	if err != nil {
-		return false, err
-	}
-	if fi, err := os.Stat(filepath.Join(filepath.Dir(v.modURI.Filename()), "vendor")); err != nil || !fi.IsDir() {
+	if fi, err := os.Stat(filepath.Join(v.rootURI.Filename(), "vendor")); err != nil || !fi.IsDir() {
 		return true, nil
 	}
 	vendorEnabled := modFile.Go.Version != "" && semver.Compare("v"+modFile.Go.Version, "v1.14") >= 0
@@ -1061,15 +1074,12 @@
 	// 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 ws.modURI == "" && len(modules) == 0 && validBuildConfiguration {
+	if len(modules) == 0 && validBuildConfiguration {
 		return mode
 	}
-	// Check if we should be using module mode.
-	if ws.modURI != "" || len(modules) > 0 {
-		mode |= moduleMode
-	}
+	mode |= moduleMode
 	// The -modfile flag is available for Go versions >= 1.14.
-	if ws.modURI != "" && options.TempModfile && ws.goversion >= 14 {
+	if options.TempModfile && ws.goversion >= 14 {
 		mode |= tempModfile
 	}
 	// Don't default to multi-workspace mode if one of the modules contains a
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index 3ae9694..549cea0 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -459,11 +459,15 @@
 	case source.Mod:
 		modFH = fh
 	case source.Go:
-		modURI, err := snapshot.GoModForFile(ctx, fh.URI())
+		modURI := snapshot.GoModForFile(ctx, fh.URI())
+		if modURI == "" {
+			return nil, nil
+		}
+		var err error
+		modFH, err = snapshot.GetFile(ctx, modURI)
 		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/diagnostics.go b/internal/lsp/diagnostics.go
index 8cebae0..9c86a31 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -130,11 +130,9 @@
 		return nil, nil
 	}
 	var (
-		showMsg     *protocol.ShowMessageParams
-		wg          sync.WaitGroup
-		seenFilesMu sync.Mutex
+		showMsg *protocol.ShowMessageParams
+		wg      sync.WaitGroup
 	)
-	seenFiles := make(map[span.URI]struct{})
 	for _, pkg := range wsPkgs {
 		wg.Add(1)
 		go func(pkg source.Package) {
@@ -143,10 +141,6 @@
 			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
 				}
@@ -198,19 +192,23 @@
 	// 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 {
+			// Check if we already have diagnostic reports for the given file,
+			// meaning that we have already seen its package.
+			var seen bool
+			for _, withAnalysis := range []bool{true, false} {
+				_, ok := reports[idWithAnalysis{
+					id:           o.VersionedFileIdentity(),
+					withAnalysis: withAnalysis,
+				}]
+				seen = seen || ok
+			}
+			if seen {
 				continue
 			}
-
-			diagnostic := s.checkForOrphanedFile(ctx, snapshot, o.URI())
+			diagnostic := s.checkForOrphanedFile(ctx, snapshot, o)
 			if diagnostic == nil {
 				continue
 			}
-			// Lock the reports map, since the per-package goroutines may
-			// not have completed yet.
 			addReport(o.VersionedFileIdentity(), true, []*source.Diagnostic{diagnostic})
 		}
 	}
@@ -220,12 +218,11 @@
 // checkForOrphanedFile checks that the given URIs can be mapped to packages.
 // If they cannot and the workspace is not otherwise unloaded, it also surfaces
 // a warning, suggesting that the user check the file for build tags.
-func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snapshot, uri span.URI) *source.Diagnostic {
-	fh, err := snapshot.GetFile(ctx, uri)
-	if err != nil || fh.Kind() != source.Go {
+func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle) *source.Diagnostic {
+	if fh.Kind() != source.Go {
 		return nil
 	}
-	pkgs, err := snapshot.PackagesForFile(ctx, uri, source.TypecheckWorkspace)
+	pkgs, err := snapshot.PackagesForFile(ctx, fh.URI(), source.TypecheckWorkspace)
 	if len(pkgs) > 0 || err == nil {
 		return nil
 	}
@@ -249,7 +246,7 @@
 		Message: fmt.Sprintf(`No packages found for open file %s: %v.
 If this file contains build tags, try adding "-tags=<build tag>" to your gopls "buildFlag" configuration (see (https://github.com/golang/tools/blob/master/gopls/doc/settings.md#buildflags-string).
 Otherwise, see the troubleshooting guidelines for help investigating (https://github.com/golang/tools/blob/master/gopls/doc/troubleshooting.md).
-`, uri.Filename(), err),
+`, fh.URI().Filename(), err),
 		Severity: protocol.SeverityWarning,
 		Source:   "compiler",
 	}
diff --git a/internal/lsp/source/completion/completion.go b/internal/lsp/source/completion/completion.go
index 4b40607..019e6a9 100644
--- a/internal/lsp/source/completion/completion.go
+++ b/internal/lsp/source/completion/completion.go
@@ -799,7 +799,7 @@
 		}
 	}
 
-	return c.snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
+	return c.snapshot.RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
 		return imports.GetImportPaths(ctx, searchImports, prefix, c.filename, c.pkg.GetTypes().Name(), opts.Env)
 	})
 }
@@ -1092,7 +1092,7 @@
 
 	var relevances map[string]int
 	if len(paths) != 0 {
-		if err := c.snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
+		if err := c.snapshot.RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
 			var err error
 			relevances, err = imports.ScoreImportPaths(ctx, opts.Env, paths)
 			return err
@@ -1153,7 +1153,7 @@
 			cancel()
 		}
 	}
-	return c.snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
+	return c.snapshot.RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
 		return imports.GetPackageExports(ctx, add, id.Name, c.filename, c.pkg.GetTypes().Name(), opts.Env)
 	})
 }
@@ -1386,7 +1386,7 @@
 
 	var relevances map[string]int
 	if len(paths) != 0 {
-		if err := c.snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
+		if err := c.snapshot.RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
 			var err error
 			relevances, err = imports.ScoreImportPaths(ctx, opts.Env, paths)
 			return err
@@ -1454,7 +1454,7 @@
 		})
 		count++
 	}
-	return c.snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
+	return c.snapshot.RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
 		return imports.GetAllCandidates(ctx, add, prefix, c.filename, c.pkg.GetTypes().Name(), opts.Env)
 	})
 }
diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go
index adb09e9..bd669b4 100644
--- a/internal/lsp/source/format.go
+++ b/internal/lsp/source/format.go
@@ -93,7 +93,7 @@
 	if err != nil {
 		return nil, nil, err
 	}
-	if err := snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
+	if err := snapshot.RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
 		allFixEdits, editsPerFix, err = computeImportEdits(ctx, snapshot, pgf, opts)
 		return err
 	}); err != nil {
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index f40a9b8..309eb7b 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -90,6 +90,10 @@
 	// snapshot's root folder will be used as the working directory.
 	RunGoCommandDirect(ctx context.Context, wd, verb string, args []string) 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
+
 	// ModFiles are the go.mod files enclosed in the snapshot's view and known
 	// to the snapshot.
 	ModFiles() []span.URI
@@ -110,7 +114,7 @@
 	ModTidy(ctx context.Context, fh FileHandle) (*TidiedModule, error)
 
 	// GoModForFile returns the URI of the go.mod file for the given URI.
-	GoModForFile(ctx context.Context, uri span.URI) (span.URI, error)
+	GoModForFile(ctx context.Context, uri span.URI) span.URI
 
 	// BuildWorkspaceModFile builds the contents of mod file to be used for
 	// multi-module workspace.
@@ -185,10 +189,6 @@
 	// Shutdown closes this view, and detaches it from its session.
 	Shutdown(ctx context.Context)
 
-	// 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
-
 	// Options returns a copy of the Options for this view.
 	Options() *Options