internal/lsp: return snapshot when creating a view

Previously, we returned CheckPackageHandles when creating a new view.
Now, return the view's snapshot. Also, add a WorkspacePackageIDs
function in order to run diagnostics on them.

Fixes golang/go#35548

Change-Id: Ica7e41f5a7aa3ee9413feb4de4ee16e1825db2e1
Reviewed-on: https://go-review.googlesource.com/c/tools/+/209418
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/session.go b/internal/lsp/cache/session.go
index 062df68..ccc24b6 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -76,20 +76,20 @@
 	return s.cache
 }
 
-func (s *session) NewView(ctx context.Context, name string, folder span.URI, options source.Options) (source.View, []source.PackageHandle, error) {
+func (s *session) NewView(ctx context.Context, name string, folder span.URI, options source.Options) (source.View, source.Snapshot, error) {
 	s.viewMu.Lock()
 	defer s.viewMu.Unlock()
-	v, phs, err := s.createView(ctx, name, folder, options)
+	v, snapshot, err := s.createView(ctx, name, folder, options)
 	if err != nil {
 		return nil, nil, err
 	}
 	s.views = append(s.views, v)
 	// we always need to drop the view map
 	s.viewMap = make(map[span.URI]source.View)
-	return v, phs, nil
+	return v, snapshot, nil
 }
 
-func (s *session) createView(ctx context.Context, name string, folder span.URI, options source.Options) (*view, []source.PackageHandle, error) {
+func (s *session) createView(ctx context.Context, name string, folder span.URI, options source.Options) (*view, *snapshot, error) {
 	index := atomic.AddInt64(&viewIndex, 1)
 	// We want a true background context and not a detached context here
 	// the spans need to be unrelated and no tag values should pollute it.
@@ -131,8 +131,6 @@
 	// Preemptively load everything in this directory.
 	// TODO(matloob): Determine if this can be done in parallel with something else.
 	// Perhaps different calls to NewView can be run in parallel?
-	// TODO(matloob): By default when a new file is opened, its data is invalidated
-	// and it's loaded again. Determine if the redundant reload can be avoided.
 	v.snapshotMu.Lock()
 	defer v.snapshotMu.Unlock() // The code after the snapshot is used isn't expensive.
 	m, err := v.snapshot.load(ctx, source.DirectoryURI(folder))
@@ -141,19 +139,17 @@
 		log.Error(ctx, "failed to load snapshot", err, telemetry.Directory.Of(folder))
 		return v, nil, nil
 	}
-
 	// Prepare CheckPackageHandles for every package that's been loaded.
 	// (*snapshot).CheckPackageHandle makes the assumption that every package that's
 	// been loaded has an existing checkPackageHandle.
-	phs, err := v.snapshot.checkWorkspacePackages(ctx, m)
-	if err != nil {
+	if _, err := v.snapshot.checkWorkspacePackages(ctx, m); err != nil {
 		// Suppress all errors.
 		log.Error(ctx, "failed to check snapshot", err, telemetry.Directory.Of(folder))
 		return v, nil, nil
 	}
 
 	debug.AddView(debugView{v})
-	return v, phs, nil
+	return v, v.snapshot, nil
 }
 
 // View returns the view by name.
@@ -248,14 +244,14 @@
 	return nil
 }
 
-func (s *session) updateView(ctx context.Context, view *view, options source.Options) (*view, []source.PackageHandle, error) {
+func (s *session) updateView(ctx context.Context, view *view, options source.Options) (*view, *snapshot, error) {
 	s.viewMu.Lock()
 	defer s.viewMu.Unlock()
 	i, err := s.dropView(ctx, view)
 	if err != nil {
 		return nil, nil, err
 	}
-	v, phs, err := s.createView(ctx, view.name, view.folder, options)
+	v, snapshot, err := s.createView(ctx, view.name, view.folder, options)
 	if err != nil {
 		// we have dropped the old view, but could not create the new one
 		// this should not happen and is very bad, but we still need to clean
@@ -266,7 +262,7 @@
 	}
 	// substitute the new view into the array where the old view was
 	s.views[i] = v
-	return v, phs, nil
+	return v, snapshot, nil
 }
 
 func (s *session) dropView(ctx context.Context, view *view) (int, error) {
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 15cd0c3..0ebfc70 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -250,6 +250,16 @@
 	return phs, nil
 }
 
+func (s *snapshot) WorkspacePackageIDs(ctx context.Context) (ids []string) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	for id := range s.workspacePackages {
+		ids = append(ids, string(id))
+	}
+	return ids
+}
+
 func (s *snapshot) KnownPackages(ctx context.Context) []source.Package {
 	// TODO(matloob): This function exists because KnownImportPaths can't
 	// determine the import paths of all packages. Remove this function
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 62a0511..af7b59b 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -16,12 +16,17 @@
 	"golang.org/x/tools/internal/telemetry/trace"
 )
 
-func (s *Server) diagnoseSnapshot(snapshot source.Snapshot, phs []source.PackageHandle) {
+func (s *Server) diagnoseSnapshot(snapshot source.Snapshot) {
 	ctx := snapshot.View().BackgroundContext()
 	ctx, done := trace.StartSpan(ctx, "lsp:background-worker")
 	defer done()
 
-	for _, ph := range phs {
+	for _, id := range snapshot.WorkspacePackageIDs(ctx) {
+		ph, err := snapshot.PackageHandle(ctx, id)
+		if err != nil {
+			log.Error(ctx, "diagnoseSnapshot: no PackageHandle for workspace package", err, telemetry.Package.Of(id))
+			continue
+		}
 		if len(ph.CompiledGoFiles()) == 0 {
 			continue
 		}
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index c8c11ae..3da24d1 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -165,12 +165,12 @@
 
 	for _, folder := range folders {
 		uri := span.NewURI(folder.URI)
-		view, workspacePackages, err := s.addView(ctx, folder.Name, span.NewURI(folder.URI))
+		_, snapshot, err := s.addView(ctx, folder.Name, span.NewURI(folder.URI))
 		if err != nil {
 			viewErrors[uri] = err
 			continue
 		}
-		go s.diagnoseSnapshot(view.Snapshot(), workspacePackages)
+		go s.diagnoseSnapshot(snapshot)
 	}
 	if len(viewErrors) > 0 {
 		errMsg := fmt.Sprintf("Error loading workspace folders (expected %v, got %v)\n", len(folders), len(s.session.Views())-originalViews)
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 56e2419..162a4a5 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -40,6 +40,10 @@
 	// that this file belongs to.
 	PackageHandles(ctx context.Context, fh FileHandle) ([]PackageHandle, error)
 
+	// WorkspacePackageIDs returns the ids of the packages at the top-level
+	// of the snapshot's view.
+	WorkspacePackageIDs(ctx context.Context) []string
+
 	// GetActiveReverseDeps returns the active files belonging to the reverse
 	// dependencies of this file's package.
 	GetReverseDependencies(id string) []string
@@ -128,6 +132,8 @@
 	// belong to or be part of a dependency of the given package.
 	FindPosInPackage(pkg Package, pos token.Pos) (*ast.File, Package, error)
 
+	// FindMapperInPackage returns the mapper associated with a file that may belong to
+	// the given package or one of its dependencies.
 	FindMapperInPackage(pkg Package, uri span.URI) (*protocol.ColumnMapper, error)
 
 	// Snapshot returns the current snapshot for the view.
@@ -140,7 +146,7 @@
 // A session may have many active views at any given time.
 type Session interface {
 	// NewView creates a new View and returns it.
-	NewView(ctx context.Context, name string, folder span.URI, options Options) (View, []PackageHandle, error)
+	NewView(ctx context.Context, name string, folder span.URI, options Options) (View, Snapshot, error)
 
 	// Cache returns the cache that created this session.
 	Cache() Cache
diff --git a/internal/lsp/workspace.go b/internal/lsp/workspace.go
index 87e6bb9..0b23499 100644
--- a/internal/lsp/workspace.go
+++ b/internal/lsp/workspace.go
@@ -26,7 +26,7 @@
 	return nil
 }
 
-func (s *Server) addView(ctx context.Context, name string, uri span.URI) (source.View, []source.PackageHandle, error) {
+func (s *Server) addView(ctx context.Context, name string, uri span.URI) (source.View, source.Snapshot, error) {
 	s.stateMu.Lock()
 	state := s.state
 	s.stateMu.Unlock()
@@ -35,8 +35,9 @@
 	}
 
 	options := s.session.Options()
-	s.fetchConfig(ctx, name, uri, &options)
-
+	if err := s.fetchConfig(ctx, name, uri, &options); err != nil {
+		return nil, nil, err
+	}
 	return s.session.NewView(ctx, name, uri, options)
 }
 
@@ -44,8 +45,13 @@
 	// go through all the views getting the config
 	for _, view := range s.session.Views() {
 		options := s.session.Options()
-		s.fetchConfig(ctx, view.Name(), view.Folder(), &options)
-		view.SetOptions(ctx, options)
+		if err := s.fetchConfig(ctx, view.Name(), view.Folder(), &options); err != nil {
+			return err
+		}
+		if _, err := view.SetOptions(ctx, options); err != nil {
+			return err
+		}
+		go s.diagnoseSnapshot(view.Snapshot())
 	}
 	return nil
 }