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.