github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/internal/lsp/cache/imports.go (about) 1 // Copyright 2020 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package cache 6 7 import ( 8 "context" 9 "fmt" 10 "reflect" 11 "strings" 12 "sync" 13 "time" 14 15 "github.com/jhump/golang-x-tools/internal/event" 16 "github.com/jhump/golang-x-tools/internal/event/keys" 17 "github.com/jhump/golang-x-tools/internal/gocommand" 18 "github.com/jhump/golang-x-tools/internal/imports" 19 "github.com/jhump/golang-x-tools/internal/lsp/source" 20 ) 21 22 type importsState struct { 23 ctx context.Context 24 25 mu sync.Mutex 26 processEnv *imports.ProcessEnv 27 cleanupProcessEnv func() 28 cacheRefreshDuration time.Duration 29 cacheRefreshTimer *time.Timer 30 cachedModFileHash string 31 cachedBuildFlags []string 32 } 33 34 func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot, fn func(*imports.Options) error) error { 35 s.mu.Lock() 36 defer s.mu.Unlock() 37 38 // Find the hash of the active mod file, if any. Using the unsaved content 39 // is slightly wasteful, since we'll drop caches a little too often, but 40 // the mod file shouldn't be changing while people are autocompleting. 41 var modFileHash string 42 // If we are using 'legacyWorkspace' mode, we can just read the modfile from 43 // the snapshot. Otherwise, we need to get the synthetic workspace mod file. 44 // 45 // TODO(rfindley): we should be able to just always use the synthetic 46 // workspace module, or alternatively use the go.work file. 47 if snapshot.workspace.moduleSource == legacyWorkspace { 48 for m := range snapshot.workspace.getActiveModFiles() { // range to access the only element 49 modFH, err := snapshot.GetFile(ctx, m) 50 if err != nil { 51 return err 52 } 53 modFileHash = modFH.FileIdentity().Hash 54 } 55 } else { 56 modFile, err := snapshot.workspace.modFile(ctx, snapshot) 57 if err != nil { 58 return err 59 } 60 modBytes, err := modFile.Format() 61 if err != nil { 62 return err 63 } 64 modFileHash = hashContents(modBytes) 65 } 66 67 // view.goEnv is immutable -- changes make a new view. Options can change. 68 // We can't compare build flags directly because we may add -modfile. 69 snapshot.view.optionsMu.Lock() 70 localPrefix := snapshot.view.options.Local 71 currentBuildFlags := snapshot.view.options.BuildFlags 72 changed := !reflect.DeepEqual(currentBuildFlags, s.cachedBuildFlags) || 73 snapshot.view.options.VerboseOutput != (s.processEnv.Logf != nil) || 74 modFileHash != s.cachedModFileHash 75 snapshot.view.optionsMu.Unlock() 76 77 // If anything relevant to imports has changed, clear caches and 78 // update the processEnv. Clearing caches blocks on any background 79 // scans. 80 if changed { 81 // As a special case, skip cleanup the first time -- we haven't fully 82 // initialized the environment yet and calling GetResolver will do 83 // unnecessary work and potentially mess up the go.mod file. 84 if s.cleanupProcessEnv != nil { 85 if resolver, err := s.processEnv.GetResolver(); err == nil { 86 if modResolver, ok := resolver.(*imports.ModuleResolver); ok { 87 modResolver.ClearForNewMod() 88 } 89 } 90 s.cleanupProcessEnv() 91 } 92 s.cachedModFileHash = modFileHash 93 s.cachedBuildFlags = currentBuildFlags 94 var err error 95 s.cleanupProcessEnv, err = s.populateProcessEnv(ctx, snapshot) 96 if err != nil { 97 return err 98 } 99 } 100 101 // Run the user function. 102 opts := &imports.Options{ 103 // Defaults. 104 AllErrors: true, 105 Comments: true, 106 Fragment: true, 107 FormatOnly: false, 108 TabIndent: true, 109 TabWidth: 8, 110 Env: s.processEnv, 111 LocalPrefix: localPrefix, 112 } 113 114 if err := fn(opts); err != nil { 115 return err 116 } 117 118 if s.cacheRefreshTimer == nil { 119 // Don't refresh more than twice per minute. 120 delay := 30 * time.Second 121 // Don't spend more than a couple percent of the time refreshing. 122 if adaptive := 50 * s.cacheRefreshDuration; adaptive > delay { 123 delay = adaptive 124 } 125 s.cacheRefreshTimer = time.AfterFunc(delay, s.refreshProcessEnv) 126 } 127 128 return nil 129 } 130 131 // populateProcessEnv sets the dynamically configurable fields for the view's 132 // process environment. Assumes that the caller is holding the s.view.importsMu. 133 func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapshot) (cleanup func(), err error) { 134 pe := s.processEnv 135 136 if snapshot.view.Options().VerboseOutput { 137 pe.Logf = func(format string, args ...interface{}) { 138 event.Log(ctx, fmt.Sprintf(format, args...)) 139 } 140 } else { 141 pe.Logf = nil 142 } 143 144 // Take an extra reference to the snapshot so that its workspace directory 145 // (if any) isn't destroyed while we're using it. 146 release := snapshot.generation.Acquire() 147 _, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{ 148 WorkingDir: snapshot.view.rootURI.Filename(), 149 }) 150 if err != nil { 151 return nil, err 152 } 153 pe.WorkingDir = inv.WorkingDir 154 pe.BuildFlags = inv.BuildFlags 155 pe.WorkingDir = inv.WorkingDir 156 pe.ModFile = inv.ModFile 157 pe.ModFlag = inv.ModFlag 158 pe.Env = map[string]string{} 159 for _, kv := range inv.Env { 160 split := strings.SplitN(kv, "=", 2) 161 if len(split) != 2 { 162 continue 163 } 164 pe.Env[split[0]] = split[1] 165 } 166 167 return func() { 168 cleanupInvocation() 169 release() 170 }, nil 171 } 172 173 func (s *importsState) refreshProcessEnv() { 174 start := time.Now() 175 176 s.mu.Lock() 177 env := s.processEnv 178 if resolver, err := s.processEnv.GetResolver(); err == nil { 179 resolver.ClearForNewScan() 180 } 181 s.mu.Unlock() 182 183 event.Log(s.ctx, "background imports cache refresh starting") 184 if err := imports.PrimeCache(context.Background(), env); err == nil { 185 event.Log(s.ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start))) 186 } else { 187 event.Log(s.ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), keys.Err.Of(err)) 188 } 189 s.mu.Lock() 190 s.cacheRefreshDuration = time.Since(start) 191 s.cacheRefreshTimer = nil 192 s.mu.Unlock() 193 } 194 195 func (s *importsState) destroy() { 196 s.mu.Lock() 197 if s.cleanupProcessEnv != nil { 198 s.cleanupProcessEnv() 199 } 200 s.mu.Unlock() 201 }