internal/lsp: fix view rebuilding when `go mod init` runs

https://github.com/microsoft/vscode-go/issues/3076#issuecomment-605062933
inspired me to write a regression test for this case. Turns out we
weren't handling it correctly after all...

This change makes sure that we only rebuild the view once a new go.mod
file is saved, not just created. It also preserves the snapshot ID
number when the view is recreated so that diagnostic caching continues
to work as expected.

Change-Id: I63bd559c3bd33b91828171cd7ddb3d099c31cddb
Reviewed-on: https://go-review.googlesource.com/c/tools/+/226017
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index bcf8dfd..3917c92 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -91,7 +91,7 @@
 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, snapshot, err := s.createView(ctx, name, folder, options)
+	v, snapshot, err := s.createView(ctx, name, folder, options, 0)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -101,7 +101,7 @@
 	return v, snapshot, nil
 }
 
-func (s *Session) createView(ctx context.Context, name string, folder span.URI, options source.Options) (*view, *snapshot, error) {
+func (s *Session) createView(ctx context.Context, name string, folder span.URI, options source.Options, snapshotID uint64) (*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.
@@ -121,6 +121,7 @@
 		filesByURI:    make(map[span.URI]*fileBase),
 		filesByBase:   make(map[string][]*fileBase),
 		snapshot: &snapshot{
+			id:                snapshotID,
 			packages:          make(map[packageKey]*packageHandle),
 			ids:               make(map[span.URI][]packageID),
 			metadata:          make(map[packageID]*metadata),
@@ -255,7 +256,11 @@
 	if err != nil {
 		return nil, nil, err
 	}
-	v, snapshot, err := s.createView(ctx, view.name, view.folder, options)
+	// Preserve the snapshot ID if we are recreating the view.
+	view.snapshotMu.Lock()
+	snapshotID := view.snapshot.id
+	view.snapshotMu.Unlock()
+	v, snapshot, err := s.createView(ctx, view.name, view.folder, options, snapshotID)
 	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
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index c0170e8..d0f1711 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -61,7 +61,7 @@
 		return nil
 	}
 	if err != nil {
-		event.Error(ctx, "warning: diagnose go.mod", err, tag.Directory.Of(snapshot.View().Folder))
+		event.Error(ctx, "warning: diagnose go.mod", err, tag.Directory.Of(snapshot.View().Folder().Filename()))
 	}
 	// Ensure that the reports returned from mod.Diagnostics are only related to the
 	// go.mod file for the module.
@@ -143,7 +143,6 @@
 		if ctx.Err() != nil {
 			break
 		}
-
 		// Pre-sort diagnostics to avoid extra work when we compare them.
 		source.SortDiagnostics(diagnostics)
 		toSend := sentDiagnostics{
@@ -181,7 +180,6 @@
 			// Do not update the delivered map since it already contains better diagnostics.
 			continue
 		}
-
 		if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
 			Diagnostics: toProtocolDiagnostics(diagnostics),
 			URI:         protocol.URIFromSpanURI(key.id.URI),
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
index 3cd6305..2a4557b 100644
--- a/internal/lsp/fake/editor.go
+++ b/internal/lsp/fake/editor.go
@@ -118,7 +118,6 @@
 	// TODO: set client capabilities.
 	params.Trace = "messages"
 	// TODO: support workspace folders.
-
 	if e.server != nil {
 		resp, err := e.server.Initialize(ctx, params)
 		if err != nil {
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index d4ad606..a4c62ac 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -26,7 +26,6 @@
 	if realURI == "" || tempURI == "" {
 		return nil, nil, nil
 	}
-
 	ctx, done := event.StartSpan(ctx, "mod.Diagnostics", tag.URI.Of(realURI))
 	defer done()
 
@@ -42,7 +41,6 @@
 	if err != nil {
 		return nil, nil, err
 	}
-
 	reports := map[source.FileIdentity][]source.Diagnostic{
 		realfh.Identity(): {},
 	}
diff --git a/internal/lsp/regtest/diagnostics_test.go b/internal/lsp/regtest/diagnostics_test.go
index 84a7512..7123db3 100644
--- a/internal/lsp/regtest/diagnostics_test.go
+++ b/internal/lsp/regtest/diagnostics_test.go
@@ -142,3 +142,44 @@
 		)
 	})
 }
+
+const noMod = `
+-- main.go --
+package main
+
+import "mod.com/bob"
+
+func main() {
+	bob.Hello()
+}
+`
+
+// TestNoMod confirms that gopls continues to work when a user adds a go.mod
+// file to their workspace.
+func TestNoMod(t *testing.T) {
+	runner.Run(t, noMod, func(env *Env) {
+		env.Await(
+			env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`),
+		)
+		env.CreateBuffer("bob/bob.go", `package bob
+
+func Hello() {
+	var x int
+}
+`)
+		env.Await(
+			env.DiagnosticAtRegexp("bob/bob.go", "x"),
+		)
+		// Save this file because otherwise, the go command will not be able to
+		// resolve mod.com/bob as a package.
+		env.SaveBuffer("bob/bob.go")
+		env.CreateBuffer("go.mod", `module mod.com
+
+go 1.12
+`)
+		env.SaveBuffer("go.mod")
+		env.Await(
+			EmptyDiagnostics("main.go"),
+		)
+	})
+}
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index 1ebe8be..6ef7a61 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -203,27 +203,29 @@
 		snapshotSet[snapshot] = append(snapshotSet[snapshot], uri)
 	}
 	for snapshot, uris := range snapshotSet {
-		for _, uri := range uris {
-			fh, err := snapshot.GetFile(uri)
-			if err != nil {
-				return nil, err
-			}
-			// If a modification comes in for the view's go.mod file and the view
-			// was never properly initialized, or the view does not have
-			// a go.mod file, try to recreate the associated view.
-			switch fh.Identity().Kind {
-			case source.Mod:
-				modfile, _ := snapshot.View().ModFiles()
-				if modfile != "" || fh.Identity().URI != modfile {
+		// If a modification comes in for the view's go.mod file and the view
+		// was never properly initialized, or the view does not have
+		// a go.mod file, try to recreate the associated view.
+		if modfile, _ := snapshot.View().ModFiles(); modfile == "" {
+			for _, uri := range uris {
+				// Don't rebuild the view until the go.mod is on disk.
+				if !snapshot.IsSaved(uri) {
 					continue
 				}
-				newSnapshot, err := snapshot.View().Rebuild(ctx)
+				fh, err := snapshot.GetFile(uri)
 				if err != nil {
 					return nil, err
 				}
-				// Update the snapshot to the rebuilt one.
-				snapshot = newSnapshot
-				snapshotByURI[uri] = snapshot
+				switch fh.Identity().Kind {
+				case source.Mod:
+					newSnapshot, err := snapshot.View().Rebuild(ctx)
+					if err != nil {
+						return nil, err
+					}
+					// Update the snapshot to the rebuilt one.
+					snapshot = newSnapshot
+					snapshotByURI[uri] = newSnapshot
+				}
 			}
 		}
 		go s.diagnoseSnapshot(snapshot)