internal/lsp/cache: extract goimports code

Many of the View's fields are for goimports, which is a good sign it
should be extracted into a separate struct. Do so in preparation for
redesigning the lifecycle.

I'm not certain this is the right direction but I don't want to deal
with a zillion merge conflicts as I figure it out. I'll move it back
later if it does turn out to have been a mistake.

No functional changes intended, just moved stuff around.

Change-Id: Ide4c2002133d00f6aaa92d114dae2b2ea3ad18fc
Reviewed-on: https://go-review.googlesource.com/c/tools/+/260557
Trust: Heschi Kreinick <heschi@google.com>
Run-TryBot: Heschi Kreinick <heschi@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/imports.go b/internal/lsp/cache/imports.go
new file mode 100644
index 0000000..6359115
--- /dev/null
+++ b/internal/lsp/cache/imports.go
@@ -0,0 +1,202 @@
+package cache
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"sync"
+	"time"
+
+	"golang.org/x/tools/internal/event"
+	"golang.org/x/tools/internal/event/keys"
+	"golang.org/x/tools/internal/imports"
+	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/span"
+)
+
+type importsState struct {
+	view *View
+	ctx  context.Context
+
+	mu                      sync.Mutex
+	processEnv              *imports.ProcessEnv
+	cleanupProcessEnv       func()
+	cacheRefreshDuration    time.Duration
+	cacheRefreshTimer       *time.Timer
+	cachedModFileIdentifier string
+	cachedBuildFlags        []string
+}
+
+func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot, fn func(*imports.Options) error) error {
+	s.mu.Lock()
+	defer s.mu.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
+	// transient changes.
+	var modFH, sumFH source.FileHandle
+	var modFileIdentifier string
+	var err error
+	// 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 snapshot.modules {
+		if m.rootURI == s.view.rootURI {
+			match = m
+		}
+	}
+	if match != nil {
+		modFH, err = snapshot.GetFile(ctx, match.modURI)
+		if err != nil {
+			return err
+		}
+		modFileIdentifier = modFH.FileIdentity().Hash
+		if match.sumURI != "" {
+			sumFH, err = snapshot.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.
+	s.view.optionsMu.Lock()
+	localPrefix := s.view.options.Local
+	currentBuildFlags := s.view.options.BuildFlags
+	changed := !reflect.DeepEqual(currentBuildFlags, s.cachedBuildFlags) ||
+		s.view.options.VerboseOutput != (s.processEnv.Logf != nil) ||
+		modFileIdentifier != s.cachedModFileIdentifier
+	s.view.optionsMu.Unlock()
+
+	// If anything relevant to imports has changed, clear caches and
+	// update the processEnv. Clearing caches blocks on any background
+	// scans.
+	if changed {
+		// 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 s.cleanupProcessEnv != nil {
+			if resolver, err := s.processEnv.GetResolver(); err == nil {
+				resolver.(*imports.ModuleResolver).ClearForNewMod()
+			}
+			s.cleanupProcessEnv()
+		}
+		s.cachedModFileIdentifier = modFileIdentifier
+		s.cachedBuildFlags = currentBuildFlags
+		s.cleanupProcessEnv, err = s.populateProcessEnv(ctx, modFH, sumFH)
+		if err != nil {
+			return err
+		}
+	}
+
+	// Run the user function.
+	opts := &imports.Options{
+		// Defaults.
+		AllErrors:   true,
+		Comments:    true,
+		Fragment:    true,
+		FormatOnly:  false,
+		TabIndent:   true,
+		TabWidth:    8,
+		Env:         s.processEnv,
+		LocalPrefix: localPrefix,
+	}
+
+	if err := fn(opts); err != nil {
+		return err
+	}
+
+	if s.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 * s.cacheRefreshDuration; adaptive > delay {
+			delay = adaptive
+		}
+		s.cacheRefreshTimer = time.AfterFunc(delay, s.refreshProcessEnv)
+	}
+
+	return nil
+}
+
+// populateProcessEnv sets the dynamically configurable fields for the view's
+// process environment. Assumes that the caller is holding the s.view.importsMu.
+func (s *importsState) populateProcessEnv(ctx context.Context, modFH, sumFH source.FileHandle) (cleanup func(), err error) {
+	cleanup = func() {}
+	pe := s.processEnv
+
+	s.view.optionsMu.Lock()
+	pe.BuildFlags = append([]string(nil), s.view.options.BuildFlags...)
+	if s.view.options.VerboseOutput {
+		pe.Logf = func(format string, args ...interface{}) {
+			event.Log(ctx, fmt.Sprintf(format, args...))
+		}
+	} else {
+		pe.Logf = nil
+	}
+	s.view.optionsMu.Unlock()
+
+	pe.Env = map[string]string{}
+	for k, v := range s.view.goEnv {
+		pe.Env[k] = v
+	}
+	pe.Env["GO111MODULE"] = s.view.go111module
+
+	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 := s.view.needsModEqualsMod(ctx, modURI, modContent)
+	if err != nil {
+		return cleanup, err
+	}
+	if modmod {
+		// -mod isn't really a build flag, but we can get away with it given
+		// the set of commands that goimports wants to run.
+		pe.BuildFlags = append([]string{"-mod=mod"}, pe.BuildFlags...)
+	}
+
+	// Add -modfile to the build flags, if we are using it.
+	if s.view.workspaceMode&tempModfile != 0 && modFH != nil {
+		var tmpURI span.URI
+		tmpURI, cleanup, err = tempModFile(modFH, sumFH)
+		if err != nil {
+			return nil, err
+		}
+		pe.BuildFlags = append(pe.BuildFlags, fmt.Sprintf("-modfile=%s", tmpURI.Filename()))
+	}
+
+	return cleanup, nil
+}
+
+func (s *importsState) refreshProcessEnv() {
+	start := time.Now()
+
+	s.mu.Lock()
+	env := s.processEnv
+	if resolver, err := s.processEnv.GetResolver(); err == nil {
+		resolver.ClearForNewScan()
+	}
+	s.mu.Unlock()
+
+	event.Log(s.ctx, "background imports cache refresh starting")
+	if err := imports.PrimeCache(context.Background(), env); err == nil {
+		event.Log(s.ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)))
+	} else {
+		event.Log(s.ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), keys.Err.Of(err))
+	}
+	s.mu.Lock()
+	s.cacheRefreshDuration = time.Since(start)
+	s.cacheRefreshTimer = nil
+	s.mu.Unlock()
+}
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index e74a0d1..58c29e3 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -185,26 +185,30 @@
 	backgroundCtx, cancel := context.WithCancel(baseCtx)
 
 	v := &View{
-		session:            s,
-		initialized:        make(chan struct{}),
-		initializationSema: make(chan struct{}, 1),
-		initializeOnce:     &sync.Once{},
-		id:                 strconv.FormatInt(index, 10),
-		options:            options,
-		baseCtx:            baseCtx,
-		backgroundCtx:      backgroundCtx,
-		cancel:             cancel,
-		name:               name,
-		folder:             folder,
-		filesByURI:         make(map[span.URI]*fileBase),
-		filesByBase:        make(map[string][]*fileBase),
+		session:              s,
+		initialized:          make(chan struct{}),
+		initializationSema:   make(chan struct{}, 1),
+		initializeOnce:       &sync.Once{},
+		id:                   strconv.FormatInt(index, 10),
+		options:              options,
+		baseCtx:              baseCtx,
+		backgroundCtx:        backgroundCtx,
+		cancel:               cancel,
+		name:                 name,
+		folder:               folder,
+		filesByURI:           make(map[span.URI]*fileBase),
+		filesByBase:          make(map[string][]*fileBase),
+		workspaceMode:        mode,
+		workspaceInformation: *ws,
+	}
+	v.importsState = &importsState{
+		view: v,
+		ctx:  backgroundCtx,
 		processEnv: &imports.ProcessEnv{
 			GocmdRunner: s.gocmdRunner,
 			WorkingDir:  folder.Filename(),
 			Env:         ws.goEnv,
 		},
-		workspaceMode:        mode,
-		workspaceInformation: *ws,
 	}
 	v.snapshot = &snapshot{
 		id:                snapshotID,
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 9179709..43eeace 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -20,12 +20,10 @@
 	"sort"
 	"strings"
 	"sync"
-	"time"
 
 	"golang.org/x/mod/modfile"
 	"golang.org/x/mod/semver"
 	"golang.org/x/tools/internal/event"
-	"golang.org/x/tools/internal/event/keys"
 	"golang.org/x/tools/internal/gocommand"
 	"golang.org/x/tools/internal/imports"
 	"golang.org/x/tools/internal/lsp/source"
@@ -63,23 +61,7 @@
 	// folder is the folder with which this view was constructed.
 	folder span.URI
 
-	// importsMu guards imports-related state, particularly the ProcessEnv.
-	importsMu sync.Mutex
-
-	// processEnv is the process env for this view.
-	// Some of its fields can be changed dynamically by modifications to
-	// the view's options. These fields are repopulated for every use.
-	// Note: this contains cached module and filesystem state.
-	//
-	// TODO(suzmue): the state cached in the process env is specific to each view,
-	// however, there is state that can be shared between views that is not currently
-	// cached, like the module cache.
-	processEnv              *imports.ProcessEnv
-	cleanupProcessEnv       func()
-	cacheRefreshDuration    time.Duration
-	cacheRefreshTimer       *time.Timer
-	cachedModFileIdentifier string
-	cachedBuildFlags        []string
+	importsState *importsState
 
 	// keep track of files by uri and by basename, a single file may be mapped
 	// to multiple uris, and the same basename may map to multiple files
@@ -362,178 +344,7 @@
 }
 
 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
-	// transient changes.
-	var modFH, sumFH source.FileHandle
-	var modFileIdentifier string
-	var err error
-	// 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 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.
-	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
-	// scans.
-	if changed {
-		// 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 s.view.cleanupProcessEnv != nil {
-			if resolver, err := s.view.processEnv.GetResolver(); err == nil {
-				resolver.(*imports.ModuleResolver).ClearForNewMod()
-			}
-			s.view.cleanupProcessEnv()
-		}
-		s.view.cachedModFileIdentifier = modFileIdentifier
-		s.view.cachedBuildFlags = currentBuildFlags
-		s.view.cleanupProcessEnv, err = s.view.populateProcessEnv(ctx, modFH, sumFH)
-		if err != nil {
-			return err
-		}
-	}
-
-	// Run the user function.
-	opts := &imports.Options{
-		// Defaults.
-		AllErrors:   true,
-		Comments:    true,
-		Fragment:    true,
-		FormatOnly:  false,
-		TabIndent:   true,
-		TabWidth:    8,
-		Env:         s.view.processEnv,
-		LocalPrefix: localPrefix,
-	}
-
-	if err := fn(opts); err != nil {
-		return err
-	}
-
-	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 * s.view.cacheRefreshDuration; adaptive > delay {
-			delay = adaptive
-		}
-		s.view.cacheRefreshTimer = time.AfterFunc(delay, s.view.refreshProcessEnv)
-	}
-
-	return nil
-}
-
-func (v *View) refreshProcessEnv() {
-	start := time.Now()
-
-	v.importsMu.Lock()
-	env := v.processEnv
-	if resolver, err := v.processEnv.GetResolver(); err == nil {
-		resolver.ClearForNewScan()
-	}
-	v.importsMu.Unlock()
-
-	// We don't have a context handy to use for logging, so use the stdlib for now.
-	event.Log(v.baseCtx, "background imports cache refresh starting")
-	if err := imports.PrimeCache(context.Background(), env); err == nil {
-		event.Log(v.baseCtx, fmt.Sprintf("background refresh finished after %v", time.Since(start)))
-	} else {
-		event.Log(v.baseCtx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), keys.Err.Of(err))
-	}
-	v.importsMu.Lock()
-	v.cacheRefreshDuration = time.Since(start)
-	v.cacheRefreshTimer = nil
-	v.importsMu.Unlock()
-}
-
-// populateProcessEnv sets the dynamically configurable fields for the view's
-// process environment. Assumes that the caller is holding the s.view.importsMu.
-func (v *View) populateProcessEnv(ctx context.Context, modFH, sumFH source.FileHandle) (cleanup func(), err error) {
-	cleanup = func() {}
-	pe := v.processEnv
-
-	v.optionsMu.Lock()
-	pe.BuildFlags = append([]string(nil), v.options.BuildFlags...)
-	if v.options.VerboseOutput {
-		pe.Logf = func(format string, args ...interface{}) {
-			event.Log(ctx, fmt.Sprintf(format, args...))
-		}
-	} else {
-		pe.Logf = nil
-	}
-	v.optionsMu.Unlock()
-
-	pe.Env = map[string]string{}
-	for k, v := range v.goEnv {
-		pe.Env[k] = v
-	}
-	pe.Env["GO111MODULE"] = v.go111module
-
-	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
-	}
-	if modmod {
-		// -mod isn't really a build flag, but we can get away with it given
-		// the set of commands that goimports wants to run.
-		pe.BuildFlags = append([]string{"-mod=mod"}, pe.BuildFlags...)
-	}
-
-	// Add -modfile to the build flags, if we are using it.
-	if v.workspaceMode&tempModfile != 0 && modFH != nil {
-		var tmpURI span.URI
-		tmpURI, cleanup, err = tempModFile(modFH, sumFH)
-		if err != nil {
-			return nil, err
-		}
-		pe.BuildFlags = append(pe.BuildFlags, fmt.Sprintf("-modfile=%s", tmpURI.Filename()))
-	}
-
-	return cleanup, nil
+	return s.view.importsState.runProcessEnvFunc(ctx, s, fn)
 }
 
 // envLocked returns the environment and build flags for the current view.