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  }