internal/lsp: support go.work outside of experimental

This change handles the case of the user creating a go.work file
during the gopls session. It also defaults to go.work/gopls.mod being
used outside of experimental mode, so that you don't have to both set
a setting and have a file.

Change-Id: If118cd2fc95c1b5600a6c06217a3b61605b11e28
Reviewed-on: https://go-review.googlesource.com/c/tools/+/342170
Trust: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go
index 4c5d1fc..456a5d1 100644
--- a/gopls/internal/regtest/workspace/workspace_test.go
+++ b/gopls/internal/regtest/workspace/workspace_test.go
@@ -656,7 +656,6 @@
 `
 	WithOptions(
 		ProxyFiles(workspaceModuleProxy),
-		Modes(Experimental),
 	).Run(t, multiModule, func(t *testing.T, env *Env) {
 		// Initially, the gopls.mod should cause only the a.com module to be
 		// loaded. Validate this by jumping to a definition in b.com and ensuring
@@ -1081,3 +1080,45 @@
 		)
 	})
 }
+
+func TestAddGoWork(t *testing.T) {
+	const nomod = `
+-- a/go.mod --
+module a.com
+
+go 1.16
+-- a/main.go --
+package main
+
+func main() {}
+-- b/go.mod --
+module b.com
+
+go 1.16
+-- b/main.go --
+package main
+
+func main() {}
+`
+	WithOptions(
+		Modes(Singleton),
+	).Run(t, nomod, func(t *testing.T, env *Env) {
+		env.OpenFile("a/main.go")
+		env.OpenFile("b/main.go")
+		env.Await(
+			DiagnosticAt("a/main.go", 0, 0),
+			DiagnosticAt("b/main.go", 0, 0),
+		)
+		env.WriteWorkspaceFile("go.work", `go 1.16
+
+directory (
+	a
+	b
+)
+`)
+		env.Await(
+			EmptyDiagnostics("a/main.go"),
+			EmptyDiagnostics("b/main.go"),
+		)
+	})
+}
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 7744f9e..453db5a 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -206,7 +206,7 @@
 		return mode
 	}
 	// The workspace module has been disabled by the user.
-	if !options.ExperimentalWorkspaceModule {
+	if s.workspace.moduleSource != goWorkWorkspace && s.workspace.moduleSource != goplsModWorkspace && !options.ExperimentalWorkspaceModule {
 		return mode
 	}
 	mode |= usesWorkspaceModule
diff --git a/internal/lsp/cache/workspace.go b/internal/lsp/cache/workspace.go
index 4204bcc..bb9125b 100644
--- a/internal/lsp/cache/workspace.go
+++ b/internal/lsp/cache/workspace.go
@@ -88,13 +88,11 @@
 }
 
 func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, excludePath func(string) bool, go111moduleOff bool, experimental bool) (*workspace, error) {
-	// In experimental mode, the user may have a gopls.mod file that defines
-	// their workspace.
-	if experimental {
-		ws, err := parseExplicitWorkspaceFile(ctx, root, fs, excludePath)
-		if err == nil {
-			return ws, nil
-		}
+	// The user may have a gopls.mod or go.work file that defines their
+	// workspace.
+	ws, err := parseExplicitWorkspaceFile(ctx, root, fs, excludePath)
+	if err == nil {
+		return ws, nil
 	}
 	// Otherwise, in all other modes, search for all of the go.mod files in the
 	// workspace.
@@ -296,75 +294,19 @@
 
 	// First handle changes to the go.work or gopls.mod file. This must be
 	// considered before any changes to go.mod or go.sum files, as these files
-	// determine which modules we care about. In legacy workspace mode we don't
-	// consider the gopls.mod or go.work files.
-	if w.moduleSource != legacyWorkspace {
-		// If go.work/gopls.mod has changed we need to either re-read it if it
-		// exists or walk the filesystem if it has been deleted.
-		// go.work should override the gopls.mod if both exist.
-		for _, src := range []workspaceSource{goplsModWorkspace, goWorkWorkspace} {
-			uri := uriForSource(w.root, src)
-			// File opens/closes are just no-ops.
-			change, ok := changes[uri]
-			if !ok || change.isUnchanged {
-				continue
-			}
-			if change.exists {
-				// Only invalidate if the file if it actually parses.
-				// Otherwise, stick with the current file.
-				var parsedFile *modfile.File
-				var parsedModules map[span.URI]struct{}
-				var err error
-				switch src {
-				case goWorkWorkspace:
-					parsedFile, parsedModules, err = parseGoWork(ctx, w.root, uri, change.content, fs)
-				case goplsModWorkspace:
-					parsedFile, parsedModules, err = parseGoplsMod(w.root, uri, change.content)
-				}
-				if err == nil {
-					changed = true
-					reload = change.fileHandle.Saved()
-					result.mod = parsedFile
-					result.moduleSource = src
-					result.knownModFiles = parsedModules
-					result.activeModFiles = make(map[span.URI]struct{})
-					for k, v := range parsedModules {
-						result.activeModFiles[k] = v
-					}
-				} else {
-					// An unparseable file should not invalidate the workspace:
-					// nothing good could come from changing the workspace in
-					// this case.
-					event.Error(ctx, fmt.Sprintf("parsing %s", filepath.Base(uri.Filename())), err)
-				}
-			} else {
-				// go.work/gopls.mod is deleted. search for modules again.
-				changed = true
-				reload = true
-				result.moduleSource = fileSystemWorkspace
-				// The parsed file is no longer valid.
-				result.mod = nil
-				knownModFiles, err := findModules(w.root, w.excludePath, 0)
-				if err != nil {
-					result.knownModFiles = nil
-					result.activeModFiles = nil
-					event.Error(ctx, "finding file system modules", err)
-				} else {
-					result.knownModFiles = knownModFiles
-					result.activeModFiles = make(map[span.URI]struct{})
-					for k, v := range result.knownModFiles {
-						result.activeModFiles[k] = v
-					}
-				}
-			}
-		}
+	// determine which modules we care about. If go.work/gopls.mod has changed
+	// we need to either re-read it if it exists or walk the filesystem if it
+	// has been deleted. go.work should override the gopls.mod if both exist.
+	if changedInner, reloadInner, found := updateExplicitWorkspaceFile(ctx, w, result, changes, fs); found {
+		changed = changedInner
+		reload = reloadInner
 	}
-
 	// Next, handle go.mod changes that could affect our workspace. If we're
 	// reading our tracked modules from the gopls.mod, there's nothing to do
 	// here.
 	if result.moduleSource != goplsModWorkspace && result.moduleSource != goWorkWorkspace {
 		for uri, change := range changes {
+			// Otherwise, we only care about go.mod files in the workspace directory.
 			if change.isUnchanged || !isGoMod(uri) || !source.InDir(result.root.Filename(), uri.Filename()) {
 				continue
 			}
@@ -407,6 +349,71 @@
 	return result, changed, reload
 }
 
+// updateExplicitWorkspaceFile checks if any of the changes happened to a go.work or
+// gopls.mod file, and if so, updates the result accordingly.
+func updateExplicitWorkspaceFile(ctx context.Context, w, result *workspace, changes map[span.URI]*fileChange, fs source.FileSource) (changed, reload, found bool) {
+	// If go.work/gopls.mod has changed we need to either re-read it if it
+	// exists or walk the filesystem if it has been deleted.
+	// go.work should override the gopls.mod if both exist.
+	for _, src := range []workspaceSource{goplsModWorkspace, goWorkWorkspace} {
+		uri := uriForSource(w.root, src)
+		// File opens/closes are just no-ops.
+		change, ok := changes[uri]
+		if !ok || change.isUnchanged {
+			continue
+		}
+		found = true
+		if change.exists {
+			// Only invalidate if the file if it actually parses.
+			// Otherwise, stick with the current file.
+			var parsedFile *modfile.File
+			var parsedModules map[span.URI]struct{}
+			var err error
+			switch src {
+			case goWorkWorkspace:
+				parsedFile, parsedModules, err = parseGoWork(ctx, w.root, uri, change.content, fs)
+			case goplsModWorkspace:
+				parsedFile, parsedModules, err = parseGoplsMod(w.root, uri, change.content)
+			}
+			if err != nil {
+				// An unparseable file should not invalidate the workspace:
+				// nothing good could come from changing the workspace in
+				// this case.
+				event.Error(ctx, fmt.Sprintf("parsing %s", filepath.Base(uri.Filename())), err)
+			}
+			changed = true
+			reload = change.fileHandle.Saved()
+			result.mod = parsedFile
+			result.moduleSource = src
+			result.knownModFiles = parsedModules
+			result.activeModFiles = make(map[span.URI]struct{})
+			for k, v := range parsedModules {
+				result.activeModFiles[k] = v
+			}
+		} else {
+			// go.work/gopls.mod is deleted. search for modules again.
+			changed = true
+			reload = true
+			result.moduleSource = fileSystemWorkspace
+			// The parsed file is no longer valid.
+			result.mod = nil
+			knownModFiles, err := findModules(w.root, w.excludePath, 0)
+			if err != nil {
+				result.knownModFiles = nil
+				result.activeModFiles = nil
+				event.Error(ctx, "finding file system modules", err)
+			} else {
+				result.knownModFiles = knownModFiles
+				result.activeModFiles = make(map[span.URI]struct{})
+				for k, v := range result.knownModFiles {
+					result.activeModFiles[k] = v
+				}
+			}
+		}
+	}
+	return changed, reload, found
+}
+
 // goplsModURI returns the URI for the gopls.mod file contained in root.
 func uriForSource(root span.URI, src workspaceSource) span.URI {
 	var basename string