internal/lsp: support unimported completions in multi-module mode

Refactor to use goCommandInvocation to get the evironment for
goimports. That takes care of most of the work; all that's left is to
track the go.mod hash so we know when to clear the cache.

Fixes golang/go#41836.

Change-Id: I614dacad04adfb879ead55e3b8640e57816534cb
Reviewed-on: https://go-review.googlesource.com/c/tools/+/270877
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: Robert Findley <rfindley@google.com>
diff --git a/internal/lsp/cache/imports.go b/internal/lsp/cache/imports.go
index 48216ab..b910aac 100644
--- a/internal/lsp/cache/imports.go
+++ b/internal/lsp/cache/imports.go
@@ -4,69 +4,65 @@
 	"context"
 	"fmt"
 	"reflect"
+	"strings"
 	"sync"
 	"time"
 
 	"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"
-	"golang.org/x/tools/internal/span"
 )
 
 type importsState struct {
 	ctx context.Context
 
-	mu                      sync.Mutex
-	processEnv              *imports.ProcessEnv
-	cleanupProcessEnv       func()
-	cacheRefreshDuration    time.Duration
-	cacheRefreshTimer       *time.Timer
-	cachedModFileIdentifier string
-	cachedBuildFlags        []string
+	mu                   sync.Mutex
+	processEnv           *imports.ProcessEnv
+	cleanupProcessEnv    func()
+	cacheRefreshDuration time.Duration
+	cacheRefreshTimer    *time.Timer
+	cachedModFileHash    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 source.FileHandle
-	var gosum []byte
-	var modFileIdentifier string
-	var err error
-	// TODO(rfindley): 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 matchURI span.URI
-	for modURI := range snapshot.workspace.activeModFiles() {
-		if dirURI(modURI) == snapshot.view.rootURI {
-			matchURI = modURI
+	// Find the hash of the active mod file, if any. Using the unsaved content
+	// is slightly wasteful, since we'll drop caches a little too often, but
+	// the mod file shouldn't be changing while people are autocompleting.
+	var modFileHash string
+	if snapshot.workspaceMode()&usesWorkspaceModule == 0 {
+		for m := range snapshot.workspace.activeModFiles() { // range to access the only element
+			modFH, err := snapshot.GetFile(ctx, m)
+			if err != nil {
+				return err
+			}
+			modFileHash = modFH.FileIdentity().Hash
 		}
-	}
-	// TODO(rFindley): should it be an error if matchURI is empty?
-	if matchURI != "" {
-		modFH, err = snapshot.GetFile(ctx, matchURI)
+	} else {
+		modFile, err := snapshot.workspace.modFile(ctx, snapshot)
 		if err != nil {
 			return err
 		}
-		modFileIdentifier = modFH.FileIdentity().Hash
-		gosum = snapshot.goSum(ctx, matchURI)
+		modBytes, err := modFile.Format()
+		if err != nil {
+			return err
+		}
+		modFileHash = hashContents(modBytes)
 	}
-	// v.goEnv is immutable -- changes make a new view. Options can change.
+
+	// view.goEnv is immutable -- changes make a new view. Options can change.
 	// We can't compare build flags directly because we may add -modfile.
 	snapshot.view.optionsMu.Lock()
 	localPrefix := snapshot.view.options.Local
 	currentBuildFlags := snapshot.view.options.BuildFlags
 	changed := !reflect.DeepEqual(currentBuildFlags, s.cachedBuildFlags) ||
 		snapshot.view.options.VerboseOutput != (s.processEnv.Logf != nil) ||
-		modFileIdentifier != s.cachedModFileIdentifier
+		modFileHash != s.cachedModFileHash
 	snapshot.view.optionsMu.Unlock()
 
 	// If anything relevant to imports has changed, clear caches and
@@ -82,9 +78,10 @@
 			}
 			s.cleanupProcessEnv()
 		}
-		s.cachedModFileIdentifier = modFileIdentifier
+		s.cachedModFileHash = modFileHash
 		s.cachedBuildFlags = currentBuildFlags
-		s.cleanupProcessEnv, err = s.populateProcessEnv(ctx, snapshot, modFH, gosum)
+		var err error
+		s.cleanupProcessEnv, err = s.populateProcessEnv(ctx, snapshot)
 		if err != nil {
 			return err
 		}
@@ -122,55 +119,41 @@
 
 // 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, snapshot *snapshot, modFH source.FileHandle, gosum []byte) (cleanup func(), err error) {
-	cleanup = func() {}
+func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapshot) (cleanup func(), err error) {
 	pe := s.processEnv
 
-	snapshot.view.optionsMu.Lock()
-	pe.BuildFlags = append([]string(nil), snapshot.view.options.BuildFlags...)
-	if snapshot.view.options.VerboseOutput {
+	if snapshot.view.Options().VerboseOutput {
 		pe.Logf = func(format string, args ...interface{}) {
 			event.Log(ctx, fmt.Sprintf(format, args...))
 		}
 	} else {
 		pe.Logf = nil
 	}
-	snapshot.view.optionsMu.Unlock()
 
+	// Take an extra reference to the snapshot so that its workspace directory
+	// (if any) isn't destroyed while we're using it.
+	release := snapshot.generation.Acquire(ctx)
+	_, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{
+		WorkingDir: snapshot.view.rootURI.Filename(),
+	})
+	pe.WorkingDir = inv.WorkingDir
+	pe.BuildFlags = inv.BuildFlags
+	pe.WorkingDir = inv.WorkingDir
+	pe.ModFile = inv.ModFile
+	pe.ModFlag = inv.ModFlag
 	pe.Env = map[string]string{}
-	for k, v := range snapshot.view.goEnv {
-		pe.Env[k] = v
-	}
-	pe.Env["GO111MODULE"] = snapshot.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
+	for _, kv := range inv.Env {
+		split := strings.SplitN(kv, "=", 2)
+		if len(split) != 2 {
+			continue
 		}
-	}
-	modmod, err := snapshot.needsModEqualsMod(ctx, modURI, modContent)
-	if err != nil {
-		return cleanup, err
-	}
-	if modmod {
-		pe.ModFlag = "mod"
+		pe.Env[split[0]] = split[1]
 	}
 
-	// Add -modfile to the build flags, if we are using it.
-	if snapshot.workspaceMode()&tempModfile != 0 && modFH != nil {
-		var tmpURI span.URI
-		tmpURI, cleanup, err = tempModFile(modFH, gosum)
-		if err != nil {
-			return nil, err
-		}
-		pe.ModFile = tmpURI.Filename()
-	}
-
-	return cleanup, nil
+	return func() {
+		cleanupInvocation()
+		release()
+	}, nil
 }
 
 func (s *importsState) refreshProcessEnv() {
@@ -194,3 +177,11 @@
 	s.cacheRefreshTimer = nil
 	s.mu.Unlock()
 }
+
+func (s *importsState) destroy() {
+	s.mu.Lock()
+	if s.cleanupProcessEnv != nil {
+		s.cleanupProcessEnv()
+	}
+	s.mu.Unlock()
+}
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index 8009df1..ec40b22 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -93,7 +93,7 @@
 	ctx, done := event.Start(ctx, "cache.view.load", tag.Query.Of(query))
 	defer done()
 
-	_, inv, cleanup, err := s.goCommandInvocation(ctx, source.ForTypeChecking, &gocommand.Invocation{
+	_, inv, cleanup, err := s.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{
 		WorkingDir: s.view.rootURI.Filename(),
 	})
 	if err != nil {
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 36bfc50..5922bf0 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -201,8 +201,6 @@
 		ctx: backgroundCtx,
 		processEnv: &imports.ProcessEnv{
 			GocmdRunner: s.gocmdRunner,
-			WorkingDir:  folder.Filename(),
-			Env:         ws.goEnv,
 		},
 	}
 	v.snapshot = &snapshot{
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 33266d7..b564796 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -260,7 +260,7 @@
 		// If we're type checking, we need to use the workspace context, meaning
 		// the main (workspace) module. Otherwise, we should use the module for
 		// the passed-in working dir.
-		if mode == source.ForTypeChecking {
+		if mode == source.LoadWorkspace {
 			if s.workspaceMode()&usesWorkspaceModule == 0 {
 				for m := range s.workspace.activeModFiles() { // range to access the only element
 					modURI = m
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 896e629..e755ed7 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -426,6 +426,7 @@
 	v.snapshotMu.Lock()
 	go v.snapshot.generation.Destroy()
 	v.snapshotMu.Unlock()
+	v.importsState.destroy()
 	if v.tempWorkspace != "" {
 		if err := os.RemoveAll(v.tempWorkspace.Filename()); err != nil {
 			event.Error(ctx, "removing temp workspace", err)
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index fa307df..5a9ee40 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -179,8 +179,9 @@
 	// modified version of the user's go.mod file, e.g. `go mod tidy` used to
 	// generate diagnostics.
 	WriteTemporaryModFile
-	// ForTypeChecking is for packages.Load.
-	ForTypeChecking
+	// LoadWorkspace is for packages.Load, and other operations that should
+	// consider the whole workspace at once.
+	LoadWorkspace
 )
 
 // View represents a single workspace.
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index 3d173b4..2456c2f 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -490,11 +490,6 @@
 			for i, e := range exp {
 				t.Run(SpanName(src)+"_"+strconv.Itoa(i), func(t *testing.T) {
 					t.Helper()
-					if strings.Contains(t.Name(), "complit") || strings.Contains(t.Name(), "UnimportedCompletion") {
-						if data.mode == "MultiModule" {
-							t.Skip("Unimported completions are broken in multi-module mode")
-						}
-					}
 					if strings.Contains(t.Name(), "cgo") {
 						testenv.NeedsTool(t, "cgo")
 					}