github.com/v2fly/tools@v0.100.0/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/v2fly/tools/internal/event" 16 "github.com/v2fly/tools/internal/event/keys" 17 "github.com/v2fly/tools/internal/gocommand" 18 "github.com/v2fly/tools/internal/imports" 19 "github.com/v2fly/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 snapshot.workspaceMode()&usesWorkspaceModule == 0 { 43 for m := range snapshot.workspace.getActiveModFiles() { // range to access the only element 44 modFH, err := snapshot.GetFile(ctx, m) 45 if err != nil { 46 return err 47 } 48 modFileHash = modFH.FileIdentity().Hash 49 } 50 } else { 51 modFile, err := snapshot.workspace.modFile(ctx, snapshot) 52 if err != nil { 53 return err 54 } 55 modBytes, err := modFile.Format() 56 if err != nil { 57 return err 58 } 59 modFileHash = hashContents(modBytes) 60 } 61 62 // view.goEnv is immutable -- changes make a new view. Options can change. 63 // We can't compare build flags directly because we may add -modfile. 64 snapshot.view.optionsMu.Lock() 65 localPrefix := snapshot.view.options.Local 66 currentBuildFlags := snapshot.view.options.BuildFlags 67 changed := !reflect.DeepEqual(currentBuildFlags, s.cachedBuildFlags) || 68 snapshot.view.options.VerboseOutput != (s.processEnv.Logf != nil) || 69 modFileHash != s.cachedModFileHash 70 snapshot.view.optionsMu.Unlock() 71 72 // If anything relevant to imports has changed, clear caches and 73 // update the processEnv. Clearing caches blocks on any background 74 // scans. 75 if changed { 76 // As a special case, skip cleanup the first time -- we haven't fully 77 // initialized the environment yet and calling GetResolver will do 78 // unnecessary work and potentially mess up the go.mod file. 79 if s.cleanupProcessEnv != nil { 80 if resolver, err := s.processEnv.GetResolver(); err == nil { 81 if modResolver, ok := resolver.(*imports.ModuleResolver); ok { 82 modResolver.ClearForNewMod() 83 } 84 } 85 s.cleanupProcessEnv() 86 } 87 s.cachedModFileHash = modFileHash 88 s.cachedBuildFlags = currentBuildFlags 89 var err error 90 s.cleanupProcessEnv, err = s.populateProcessEnv(ctx, snapshot) 91 if err != nil { 92 return err 93 } 94 } 95 96 // Run the user function. 97 opts := &imports.Options{ 98 // Defaults. 99 AllErrors: true, 100 Comments: true, 101 Fragment: true, 102 FormatOnly: false, 103 TabIndent: true, 104 TabWidth: 8, 105 Env: s.processEnv, 106 LocalPrefix: localPrefix, 107 } 108 109 if err := fn(opts); err != nil { 110 return err 111 } 112 113 if s.cacheRefreshTimer == nil { 114 // Don't refresh more than twice per minute. 115 delay := 30 * time.Second 116 // Don't spend more than a couple percent of the time refreshing. 117 if adaptive := 50 * s.cacheRefreshDuration; adaptive > delay { 118 delay = adaptive 119 } 120 s.cacheRefreshTimer = time.AfterFunc(delay, s.refreshProcessEnv) 121 } 122 123 return nil 124 } 125 126 // populateProcessEnv sets the dynamically configurable fields for the view's 127 // process environment. Assumes that the caller is holding the s.view.importsMu. 128 func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapshot) (cleanup func(), err error) { 129 pe := s.processEnv 130 131 if snapshot.view.Options().VerboseOutput { 132 pe.Logf = func(format string, args ...interface{}) { 133 event.Log(ctx, fmt.Sprintf(format, args...)) 134 } 135 } else { 136 pe.Logf = nil 137 } 138 139 // Take an extra reference to the snapshot so that its workspace directory 140 // (if any) isn't destroyed while we're using it. 141 release := snapshot.generation.Acquire(ctx) 142 _, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{ 143 WorkingDir: snapshot.view.rootURI.Filename(), 144 }) 145 if err != nil { 146 return nil, err 147 } 148 pe.WorkingDir = inv.WorkingDir 149 pe.BuildFlags = inv.BuildFlags 150 pe.WorkingDir = inv.WorkingDir 151 pe.ModFile = inv.ModFile 152 pe.ModFlag = inv.ModFlag 153 pe.Env = map[string]string{} 154 for _, kv := range inv.Env { 155 split := strings.SplitN(kv, "=", 2) 156 if len(split) != 2 { 157 continue 158 } 159 pe.Env[split[0]] = split[1] 160 } 161 162 return func() { 163 cleanupInvocation() 164 release() 165 }, nil 166 } 167 168 func (s *importsState) refreshProcessEnv() { 169 start := time.Now() 170 171 s.mu.Lock() 172 env := s.processEnv 173 if resolver, err := s.processEnv.GetResolver(); err == nil { 174 resolver.ClearForNewScan() 175 } 176 s.mu.Unlock() 177 178 event.Log(s.ctx, "background imports cache refresh starting") 179 if err := imports.PrimeCache(context.Background(), env); err == nil { 180 event.Log(s.ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start))) 181 } else { 182 event.Log(s.ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), keys.Err.Of(err)) 183 } 184 s.mu.Lock() 185 s.cacheRefreshDuration = time.Since(start) 186 s.cacheRefreshTimer = nil 187 s.mu.Unlock() 188 } 189 190 func (s *importsState) destroy() { 191 s.mu.Lock() 192 if s.cleanupProcessEnv != nil { 193 s.cleanupProcessEnv() 194 } 195 s.mu.Unlock() 196 }