internal/lsp/cache: keep a cached workspace module dir

Keep a workspace module dir around for running the go command against a
snapshot, bound to the contents of the workspace modfile.

This uses the cache's resource model to share the workspace module dir
across snapshots if it is not invalidated, and to delete it when it is
no longer in-use by a snapshot. Of course, the go command will still
only see files on the filesystem, but using this immutable model was
most consistent with the immutable workspace.

For golang/go#41836

Change-Id: Iaec544283b2f545071e5cab1d0ff2a66e6d24dff
Reviewed-on: https://go-review.googlesource.com/c/tools/+/263938
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
Trust: Robert Findley <rfindley@google.com>
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index f5bb720..ccf0c1a 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -19,6 +19,7 @@
 	"golang.org/x/tools/internal/gocommand"
 	"golang.org/x/tools/internal/lsp/debug/tag"
 	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/memoize"
 	"golang.org/x/tools/internal/packagesinternal"
 	"golang.org/x/tools/internal/span"
 	errors "golang.org/x/xerrors"
@@ -184,37 +185,62 @@
 	return srcErrs
 }
 
-// tempWorkspaceModule creates a temporary directory for use with
-// packages.Loads that occur from within the workspace module.
-func (s *snapshot) tempWorkspaceModule(ctx context.Context) (_ span.URI, cleanup func(), err error) {
-	cleanup = func() {}
-	if s.workspaceMode()&usesWorkspaceModule == 0 {
-		return "", cleanup, nil
+type workspaceDirKey string
+
+type workspaceDirData struct {
+	dir string
+	err error
+}
+
+// getWorkspaceDir gets the URI for the workspace directory associated with
+// this snapshot. The workspace directory is a temp directory containing the
+// go.mod file computed from all active modules.
+func (s *snapshot) getWorkspaceDir(ctx context.Context) (span.URI, error) {
+	s.mu.Lock()
+	h := s.workspaceDirHandle
+	s.mu.Unlock()
+	if h != nil {
+		return getWorkspaceDir(ctx, h, s.generation)
 	}
 	file, err := s.workspace.modFile(ctx, s)
 	if err != nil {
-		return "", nil, err
+		return "", err
 	}
-
 	content, err := file.Format()
 	if err != nil {
-		return "", cleanup, err
+		return "", err
 	}
-	// Create a temporary working directory for the go command that contains
-	// the workspace module file.
-	name, err := ioutil.TempDir("", "gopls-mod")
+	key := workspaceDirKey(hashContents(content))
+	s.mu.Lock()
+	s.workspaceDirHandle = s.generation.Bind(key, func(context.Context, memoize.Arg) interface{} {
+		tmpdir, err := ioutil.TempDir("", "gopls-workspace-mod")
+		if err != nil {
+			return &workspaceDirData{err: err}
+		}
+		filename := filepath.Join(tmpdir, "go.mod")
+		if err := ioutil.WriteFile(filename, content, 0644); err != nil {
+			os.RemoveAll(tmpdir)
+			return &workspaceDirData{err: err}
+		}
+		return &workspaceDirData{dir: tmpdir}
+	}, func(v interface{}) {
+		d := v.(*workspaceDirData)
+		if d.dir != "" {
+			if err := os.RemoveAll(d.dir); err != nil {
+				event.Error(context.Background(), "cleaning workspace dir", err)
+			}
+		}
+	})
+	s.mu.Unlock()
+	return getWorkspaceDir(ctx, s.workspaceDirHandle, s.generation)
+}
+
+func getWorkspaceDir(ctx context.Context, h *memoize.Handle, g *memoize.Generation) (span.URI, error) {
+	v, err := h.Get(ctx, g, nil)
 	if err != nil {
-		return "", cleanup, err
+		return "", err
 	}
-	cleanup = func() {
-		os.RemoveAll(name)
-	}
-	filename := filepath.Join(name, "go.mod")
-	if err := ioutil.WriteFile(filename, content, 0644); err != nil {
-		cleanup()
-		return "", cleanup, err
-	}
-	return span.URIFromPath(filepath.Dir(filename)), cleanup, nil
+	return span.URIFromPath(v.(*workspaceDirData).dir), nil
 }
 
 // setMetadata extracts metadata from pkg and records it in s. It
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index e452df1..83bb665 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -92,7 +92,8 @@
 	modUpgradeHandles map[span.URI]*modUpgradeHandle
 	modWhyHandles     map[span.URI]*modWhyHandle
 
-	workspace *workspace
+	workspace          *workspace
+	workspaceDirHandle *memoize.Handle
 }
 
 type packageKey struct {
@@ -252,7 +253,7 @@
 			} else {
 				var tmpDir span.URI
 				var err error
-				tmpDir, cleanup, err = s.tempWorkspaceModule(ctx)
+				tmpDir, err = s.getWorkspaceDir(ctx)
 				if err != nil {
 					return "", nil, cleanup, err
 				}
@@ -1001,6 +1002,11 @@
 		workspace:         newWorkspace,
 	}
 
+	if !workspaceChanged && s.workspaceDirHandle != nil {
+		result.workspaceDirHandle = s.workspaceDirHandle
+		newGen.Inherit(s.workspaceDirHandle)
+	}
+
 	if s.builtin != nil {
 		newGen.Inherit(s.builtin.handle)
 	}