internal/lsp: show a warning message when opening an "orphaned" file

Build tags are a common stumbling block for users of gopls, as build
tagged files may be excluded from the initial workspace load without a
clear warning. This change adds a check for every opened file to confirm
if it maps to a package. If not, we show a message with suggestions.

A follow-up improvement might be to check if the opened file actually
has build tags to make the error message more precise (and give a better
example config).

Updates golang/go#31668

Change-Id: I829d8546edea65aa08274021bfde8ea2fb6eeaa1
Reviewed-on: https://go-review.googlesource.com/c/tools/+/253798
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/regtest/diagnostics_test.go b/gopls/internal/regtest/diagnostics_test.go
index 96d61b1..fac3f71 100644
--- a/gopls/internal/regtest/diagnostics_test.go
+++ b/gopls/internal/regtest/diagnostics_test.go
@@ -520,7 +520,7 @@
 `
 	runner.Run(t, noModule, func(t *testing.T, env *Env) {
 		env.OpenFile("a.go")
-		env.Await(env.DiagnosticAtRegexp("a.go", "fmt.Printl"), SomeShowMessage(""))
+		env.Await(env.DiagnosticAtRegexp("a.go", "fmt.Printl"), ShownMessage(""))
 	})
 }
 
@@ -1313,3 +1313,39 @@
 		}
 	})
 }
+
+func TestNotifyOrphanedFiles(t *testing.T) {
+	const files = `
+-- go.mod --
+module mod.com
+
+go 1.12
+-- a/a.go --
+package a
+
+func main() {
+	var x int
+}
+-- a/a_ignore.go --
+// +build ignore
+
+package a
+
+func _() {
+	var x int
+}
+`
+	run(t, files, func(t *testing.T, env *Env) {
+		env.Await(
+			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1),
+		)
+		env.OpenFile("a/a.go")
+		env.Await(
+			env.DiagnosticAtRegexp("a/a.go", "x"),
+		)
+		env.OpenFile("a/a_ignore.go")
+		env.Await(
+			ShownMessage("No packages found for open file"),
+		)
+	})
+}
diff --git a/gopls/internal/regtest/env.go b/gopls/internal/regtest/env.go
index a341986..262851f 100644
--- a/gopls/internal/regtest/env.go
+++ b/gopls/internal/regtest/env.go
@@ -389,8 +389,9 @@
 	}
 }
 
-// SomeShowMessage asserts that the editor has received a ShowMessage with the given title.
-func SomeShowMessage(title string) SimpleExpectation {
+// ShownMessage asserts that the editor has received a ShownMessage with the
+// given title.
+func ShownMessage(title string) SimpleExpectation {
 	check := func(s State) (Verdict, interface{}) {
 		for _, m := range s.showMessage {
 			if strings.Contains(m.Message, title) {
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index 5f8f1aa..facabbc 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -11,7 +11,9 @@
 	"path/filepath"
 	"sync"
 
+	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/jsonrpc2"
+	"golang.org/x/tools/internal/lsp/debug/tag"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
@@ -89,16 +91,13 @@
 			return err
 		}
 	}
-
-	return s.didModifyFiles(ctx, []source.FileModification{
-		{
-			URI:        uri,
-			Action:     source.Open,
-			Version:    params.TextDocument.Version,
-			Text:       []byte(params.TextDocument.Text),
-			LanguageID: params.TextDocument.LanguageID,
-		},
-	}, FromDidOpen)
+	return s.didModifyFiles(ctx, []source.FileModification{{
+		URI:        uri,
+		Action:     source.Open,
+		Version:    params.TextDocument.Version,
+		Text:       []byte(params.TextDocument.Text),
+		LanguageID: params.TextDocument.LanguageID,
+	}}, FromDidOpen)
 }
 
 func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error {
@@ -280,10 +279,17 @@
 			}
 		}
 		diagnosticWG.Add(1)
-		go func(snapshot source.Snapshot) {
+		go func(snapshot source.Snapshot, uris []span.URI) {
 			defer diagnosticWG.Done()
 			s.diagnoseSnapshot(snapshot)
-		}(snapshot)
+
+			// If files have been newly opened, check if we found packages for
+			// them. If not, notify this user that the file may be excluded
+			// because of build tags.
+			if cause == FromDidOpen {
+				s.checkForOrphanedFile(ctx, snapshot, uris)
+			}
+		}(snapshot, uris)
 	}
 
 	go func() {
@@ -307,6 +313,34 @@
 	return fmt.Sprintf("diagnosing %v", cause)
 }
 
+// checkForOrphanedFile checks that the given URIs can be mapped to packages.
+// If they cannot and the workspace is not otherwise unloaded, it also surfaces
+// a warning, suggesting that the user check the file for build tags.
+func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snapshot, uris []span.URI) {
+	// Only show the error message if we have packages in the workspace,
+	// but no package for the file.
+	if pkgs, err := snapshot.WorkspacePackages(ctx); err != nil || len(pkgs) == 0 {
+		return
+	}
+	for _, uri := range uris {
+		pkgs, err := snapshot.PackagesForFile(ctx, uri, source.TypecheckWorkspace)
+		if len(pkgs) > 0 || err == nil {
+			return
+		}
+		// TODO(rstambler): We should be able to parse the build tags in the
+		// file and show a more specific error message.
+		if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+			Type: protocol.Error,
+			Message: fmt.Sprintf(`No packages found for open file %s: %v.
+If this file contains build tags, try adding "-tags=<build tag>" to your gopls "buildFlag" configuration (see (https://github.com/golang/tools/blob/master/gopls/doc/settings.md#buildflags-string).
+Otherwise, see the troubleshooting guidelines for help investigating (https://github.com/golang/tools/blob/master/gopls/doc/troubleshooting.md).
+`, uri.Filename(), err),
+		}); err != nil {
+			event.Error(ctx, "warnAboutBuildTags: failed to show message", err, tag.URI.Of(uri))
+		}
+	}
+}
+
 func (s *Server) wasFirstChange(uri span.URI) bool {
 	s.changedFilesMu.Lock()
 	defer s.changedFilesMu.Unlock()