diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index f2d660203d..bd31dfff06 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -596,85 +596,10 @@ func (s *snapshot) workspaceScope(ctx context.Context) interface{} { } } -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 @@ func (s *snapshot) clone(ctx context.Context, withoutURI span.URI) *snapshot { 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 @@ func (s *snapshot) clone(ctx context.Context, withoutURI span.URI) *snapshot { } 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 1bb84c8f49..d480f2264e 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -594,7 +594,7 @@ func (v *view) invalidateContent(ctx context.Context, uri span.URI, kind source. 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 }