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