internal/lsp: refactor (*snapshot).clone to handle multiple URIs

This is the first in a series of changes to batch file changes. First,
we have to support invalidating a snaphot with multiple files.

Updates golang/go#31553

Change-Id: I7cd83d9280e3274549a72393bb9010c47eb5dd1e
Reviewed-on: https://go-review.googlesource.com/c/tools/+/215902
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index f2d6602..bd31dff 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -596,85 +596,10 @@
 	}
 }
 
-func (s *snapshot) clone(ctx context.Context, withoutURI span.URI) *snapshot {
+func (s *snapshot) clone(ctx context.Context, withoutURIs []span.URI) *snapshot {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
-	directIDs := map[packageID]struct{}{}
-	// Collect all of the package IDs that correspond to the given file.
-	// TODO: if the file has moved into a new package, we should invalidate that too.
-	for _, id := range s.ids[withoutURI] {
-		directIDs[id] = struct{}{}
-	}
-
-	// Get the current and original FileHandles for this URI.
-	currentFH := s.view.session.GetFile(withoutURI)
-	originalFH := s.files[withoutURI]
-
-	// Check if the file's package name or imports have changed,
-	// and if so, invalidate this file's packages' metadata.
-	invalidateMetadata := s.view.session.cache.shouldLoad(ctx, s, originalFH, currentFH)
-
-	// If a go.mod file's contents have changed, invalidate the metadata
-	// for all of the packages in the workspace.
-	if invalidateMetadata && currentFH.Identity().Kind == source.Mod {
-		for id := range s.workspacePackages {
-			directIDs[id] = struct{}{}
-		}
-	}
-
-	// If this is a file we don't yet know about,
-	// then we do not yet know what packages it should belong to.
-	// Make a rough estimate of what metadata to invalidate by finding the package IDs
-	// of all of the files in the same directory as this one.
-	// TODO(rstambler): Speed this up by mapping directories to filenames.
-	if len(directIDs) == 0 {
-		if dirStat, err := os.Stat(filepath.Dir(withoutURI.Filename())); err == nil {
-			for uri := range s.files {
-				if fdirStat, err := os.Stat(filepath.Dir(uri.Filename())); err == nil {
-					if os.SameFile(dirStat, fdirStat) {
-						for _, id := range s.ids[uri] {
-							directIDs[id] = struct{}{}
-						}
-					}
-				}
-			}
-		}
-	}
-
-	// Invalidate reverse dependencies too.
-	// TODO(heschi): figure out the locking model and use transitiveReverseDeps?
-	transitiveIDs := make(map[packageID]struct{})
-	var addRevDeps func(packageID)
-	addRevDeps = func(id packageID) {
-		if _, seen := transitiveIDs[id]; seen {
-			return
-		}
-		transitiveIDs[id] = struct{}{}
-		for _, rid := range s.getImportedByLocked(id) {
-			addRevDeps(rid)
-		}
-	}
-	for id := range directIDs {
-		addRevDeps(id)
-	}
-	// Invalidate metadata for the transitive dependencies,
-	// if they are x_tests and test variants.
-	//
-	// An example:
-	//
-	// The only way to reload the metadata for
-	// golang.org/x/tools/internal/lsp/cache [golang.org/x/tools/internal/lsp/source.test]
-	// is by reloading golang.org/x/tools/internal/lsp/source.
-	// That means we have to invalidate the metadata for
-	// golang.org/x/tools/internal/lsp/source_test when invalidating metadata for
-	// golang.org/x/tools/internal/lsp/cache.
-	for id := range transitiveIDs {
-		if m := s.metadata[id]; m != nil && m.forTest != "" {
-			directIDs[id] = struct{}{}
-		}
-	}
-
 	result := &snapshot{
 		id:                s.id + 1,
 		view:              s.view,
@@ -692,17 +617,86 @@
 		result.files[k] = v
 	}
 
-	// Handle the invalidated file; it may have new contents or not exist.
-	if _, _, err := currentFH.Read(ctx); os.IsNotExist(err) {
-		delete(result.files, withoutURI)
-	} else {
-		result.files[withoutURI] = currentFH
+	// transitiveIDs keeps track of transitive reverse dependencies.
+	// If an ID is present in the map, invalidate its types.
+	// If an ID's value is true, invalidate its metadata too.
+	transitiveIDs := make(map[packageID]bool)
+
+	for _, withoutURI := range withoutURIs {
+		directIDs := map[packageID]struct{}{}
+
+		// Collect all of the package IDs that correspond to the given file.
+		// TODO: if the file has moved into a new package, we should invalidate that too.
+		for _, id := range s.ids[withoutURI] {
+			directIDs[id] = struct{}{}
+		}
+		// Get the current and original FileHandles for this URI.
+		currentFH := s.view.session.GetFile(withoutURI)
+		originalFH := s.files[withoutURI]
+
+		// Check if the file's package name or imports have changed,
+		// and if so, invalidate this file's packages' metadata.
+		invalidateMetadata := s.view.session.cache.shouldLoad(ctx, s, originalFH, currentFH)
+
+		// If a go.mod file's contents have changed, invalidate the metadata
+		// for all of the packages in the workspace.
+		if invalidateMetadata && currentFH.Identity().Kind == source.Mod {
+			for id := range s.workspacePackages {
+				directIDs[id] = struct{}{}
+			}
+		}
+
+		// If this is a file we don't yet know about,
+		// then we do not yet know what packages it should belong to.
+		// Make a rough estimate of what metadata to invalidate by finding the package IDs
+		// of all of the files in the same directory as this one.
+		// TODO(rstambler): Speed this up by mapping directories to filenames.
+		if len(directIDs) == 0 {
+			if dirStat, err := os.Stat(filepath.Dir(withoutURI.Filename())); err == nil {
+				for uri := range s.files {
+					if fdirStat, err := os.Stat(filepath.Dir(uri.Filename())); err == nil {
+						if os.SameFile(dirStat, fdirStat) {
+							for _, id := range s.ids[uri] {
+								directIDs[id] = struct{}{}
+							}
+						}
+					}
+				}
+			}
+		}
+
+		// Invalidate reverse dependencies too.
+		// TODO(heschi): figure out the locking model and use transitiveReverseDeps?
+		var addRevDeps func(packageID)
+		addRevDeps = func(id packageID) {
+			if _, seen := transitiveIDs[id]; seen {
+				return
+			}
+			transitiveIDs[id] = invalidateMetadata
+			for _, rid := range s.getImportedByLocked(id) {
+				addRevDeps(rid)
+			}
+		}
+		for id := range directIDs {
+			addRevDeps(id)
+		}
+
+		// Handle the invalidated file; it may have new contents or not exist.
+		if _, _, err := currentFH.Read(ctx); os.IsNotExist(err) {
+			delete(result.files, withoutURI)
+		} else {
+			result.files[withoutURI] = currentFH
+		}
 	}
 
 	// Collect the IDs for the packages associated with the excluded URIs.
 	for k, ids := range s.ids {
 		result.ids[k] = ids
 	}
+	// Copy the set of initally loaded packages.
+	for k, v := range s.workspacePackages {
+		result.workspacePackages[k] = v
+	}
 	// Copy the package type information.
 	for k, v := range s.packages {
 		if _, ok := transitiveIDs[k.id]; ok {
@@ -717,21 +711,17 @@
 		}
 		result.actions[k] = v
 	}
-	// Copy the set of initally loaded packages.
-	for k, v := range s.workspacePackages {
-		result.workspacePackages[k] = v
-	}
-
 	// Copy the package metadata. We only need to invalidate packages directly
 	// containing the affected file, and only if it changed in a relevant way.
 	for k, v := range s.metadata {
-		if _, ok := directIDs[k]; invalidateMetadata && ok {
+		if invalidateMetadata, ok := transitiveIDs[k]; invalidateMetadata && ok {
 			continue
 		}
 		result.metadata[k] = v
 	}
 	// Don't bother copying the importedBy graph,
 	// as it changes each time we update metadata.
+
 	return result
 }
 
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 1bb84c8..d480f22 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -594,7 +594,7 @@
 	v.snapshotMu.Lock()
 	defer v.snapshotMu.Unlock()
 
-	v.snapshot = v.snapshot.clone(ctx, uri)
+	v.snapshot = v.snapshot.clone(ctx, []span.URI{uri})
 	return v.snapshot
 }