[gopls-release-branch.0.6] all: merge master into gopls-release-branch.0.6

9b8df07b internal/lsp: enable -mod=readonly in workspace module mode
f1f686b0 internal/lsp: re-enable upgrades for individual dependencies
d8a2a079 go/packages: improve go invocation errors
19db92ec internal/lsp/cache: remove mod upgrade code
0cef57b5 internal/lsp/protocol: use a pointer for code action's disabled field
db4c57db gopls/internal/regtest: split regtests up into multiple packages
f871472f internal/lsp/cache: lock in snapshot.knownFilesInDir
c2bea79d internal/lsp/source: make it an error to rename embedded fields
514964b7 gopls/internal/hooks: improve license file test
68bf78a6 internal/lsp/cmd: improve help output of gopls subcommands
4922717d go/analysis/passes/fieldalignment: delete doc style comments in fix
917f61df gopls/internal/regtest: automate counting of editor notifications to await
2972602e internal/lsp: correct links provided in critical error pop-ups
e13398c8 internal/lsp: display current diagnostics in the debug server
cf1022a4 gopls: factor out advanced documentation from the README
87bc10f2 gopls: mention workspaces and build systems in the README
ce34e269 internal/lsp: don't show context cancellation in the progress bar
bec622c3 gopls: merge README and user.md
7e51fbd4 gopls/internal/regtest: re-enable android builder

Change-Id: I0e262f49306c7b44d89d994dfa89659fe04b6724
diff --git a/go/analysis/passes/fieldalignment/fieldalignment.go b/go/analysis/passes/fieldalignment/fieldalignment.go
index ca1bc53..ca7ceb2 100644
--- a/go/analysis/passes/fieldalignment/fieldalignment.go
+++ b/go/analysis/passes/fieldalignment/fieldalignment.go
@@ -79,6 +79,7 @@
 		// TODO: Preserve comment, for now get rid of them.
 		//       See https://github.com/golang/go/issues/20744
 		f.Comment = nil
+		f.Doc = nil
 		if len(f.Names) <= 1 {
 			flat = append(flat, f)
 			continue
diff --git a/go/analysis/passes/fieldalignment/testdata/src/a/a.go b/go/analysis/passes/fieldalignment/testdata/src/a/a.go
index b47ee19..463b4cb 100644
--- a/go/analysis/passes/fieldalignment/testdata/src/a/a.go
+++ b/go/analysis/passes/fieldalignment/testdata/src/a/a.go
@@ -35,3 +35,12 @@
 	y int32
 	z byte
 }
+
+type WithComments struct { // want "struct of size 8 could be 4"
+	// doc style comment
+	a uint32  // field a comment
+	b [0]byte // field b comment
+	// other doc style comment
+
+	// and a last comment
+}
diff --git a/go/analysis/passes/fieldalignment/testdata/src/a/a.go.golden b/go/analysis/passes/fieldalignment/testdata/src/a/a.go.golden
index 34fc21b..c1c75e2 100644
--- a/go/analysis/passes/fieldalignment/testdata/src/a/a.go.golden
+++ b/go/analysis/passes/fieldalignment/testdata/src/a/a.go.golden
@@ -35,3 +35,8 @@
 	x byte
 	z byte
 }
+
+type WithComments struct {
+	b [0]byte
+	a uint32
+}
diff --git a/go/packages/golist.go b/go/packages/golist.go
index ec417ba..f89b05b 100644
--- a/go/packages/golist.go
+++ b/go/packages/golist.go
@@ -10,7 +10,6 @@
 	"encoding/json"
 	"fmt"
 	"go/types"
-	exec "golang.org/x/sys/execabs"
 	"io/ioutil"
 	"log"
 	"os"
@@ -23,6 +22,7 @@
 	"sync"
 	"unicode"
 
+	exec "golang.org/x/sys/execabs"
 	"golang.org/x/tools/go/internal/packagesdriver"
 	"golang.org/x/tools/internal/gocommand"
 	"golang.org/x/xerrors"
@@ -865,7 +865,7 @@
 	if gocmdRunner == nil {
 		gocmdRunner = &gocommand.Runner{}
 	}
-	stdout, stderr, _, err := gocmdRunner.RunRaw(cfg.Context, inv)
+	stdout, stderr, friendlyErr, err := gocmdRunner.RunRaw(cfg.Context, inv)
 	if err != nil {
 		// Check for 'go' executable not being found.
 		if ee, ok := err.(*exec.Error); ok && ee.Err == exec.ErrNotFound {
@@ -886,7 +886,7 @@
 
 		// Related to #24854
 		if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "unexpected directory layout") {
-			return nil, fmt.Errorf("%s", stderr.String())
+			return nil, friendlyErr
 		}
 
 		// Is there an error running the C compiler in cgo? This will be reported in the "Error" field
@@ -999,7 +999,7 @@
 		// TODO(matloob): Remove these once we can depend on go list to exit with a zero status with -e even when
 		// packages don't exist or a build fails.
 		if !usesExportData(cfg) && !containsGoFile(args) {
-			return nil, fmt.Errorf("go %v: %s: %s", args, exitErr, stderr)
+			return nil, friendlyErr
 		}
 	}
 	return stdout, nil
diff --git a/gopls/README.md b/gopls/README.md
index d2baf72..18798e1 100644
--- a/gopls/README.md
+++ b/gopls/README.md
@@ -1,73 +1,99 @@
-# gopls documentation
+# `gopls`, the Go language server
 
 [![PkgGoDev](https://pkg.go.dev/badge/golang.org/x/tools/gopls)](https://pkg.go.dev/golang.org/x/tools/gopls)
 
-gopls (pronounced: "go please") is the official [language server] for the Go language.
+`gopls` (pronounced "Go please") is the official Go [language server] developed
+by the Go team. It provides IDE features to any [LSP]-compatible editor.
 
-## Status
+<!--TODO(rstambler): Add gifs here.-->
 
-It is currently in **alpha**, so it is **not stable**.
+You should not need to interact with `gopls` directly--it will be automatically
+integrated into your editor. The specific features and settings vary slightly
+by editor, so we recommend that you proceed to the [documentation for your
+editor](#editors) below.
 
-You can see more information about the status of gopls and its supported features [here](doc/status.md).
+## Editors
 
-## Roadmap
+To get started with `gopls`, install an LSP plugin in your editor of choice.
 
-The current goal is a fully stable build with the existing feature set, aiming
-for the first half of 2020, with release candidates earlier in the year.
-
-This will be the first build that we recommend people use, and will be tagged as the 1.0 version.
-You can see the set of things being worked on in the [1.0 milestone], in general
-we are focused on stability, specifically, making sure we have a reliable service that produces an experience in module mode that is not a retrograde step from the old tools in GOPATH mode.
-
-There is also considerable effort being put into testing in order to make sure that we both have a stable service and also that we do not regress after launch.
-
-While we may continue to accept contributions for new features, they may be turned off behind a configuration flag if they are not yet stable. See the [gopls unplanned] milestone for deprioritized features.
-
-This is just a milestone for gopls itself. We work with editor integrators to make sure they can use the latest builds of gopls, and will help them use the 1.0 version as soon as it is ready, but that does not imply anything about the stability, supported features or version of the plugins.
-
-## Using
-
-In general you should not need to know anything about gopls, it should be integrated into your editor for you.
-
-To install for your specific editor you can follow the following instructions
-
-* [VSCode](doc/vscode.md)
+* [VSCode](https://github.com/golang/vscode-go/blob/master/README.md)
 * [Vim / Neovim](doc/vim.md)
 * [Emacs](doc/emacs.md)
-* [Acme](doc/acme.md)
+* [Atom](https://github.com/MordFustang21/ide-gopls)
 * [Sublime Text](doc/subl.md)
-* [Atom](doc/atom.md)
+* [Acme](https://github.com/fhs/acme-lsp)
 
-See the [user guide](doc/user.md) for more information, including the how to install gopls by hand if you need.
+If you use `gopls` with an editor that is not on this list, please let us know
+by [filing an issue](#new-issue) or [modifying this documentation](doc/contributing.md).
 
-## Issues
+## Installation
 
-If you are having issues with gopls, please first check the [known issues](doc/status.md#known-issues) before following the [troubleshooting](doc/troubleshooting.md#steps) guide.
-If that does not give you the information you need, reach out to us.
+For the most part, you should not need to install or update `gopls`. Your
+editor should handle that step for you.
 
-You can chat with us on:
-* the golang-tools [mailing list]
-* the #gopls [slack channel] on the gophers slack
+If you do want to get the latest stable version of `gopls`, change to any
+directory that is both outside of your `GOPATH` and outside of a module (a temp
+directory is fine), and run:
 
-If you think you have an issue that needs fixing, or a feature suggestion, then please make sure you follow the steps to [file an issue](doc/troubleshooting.md#file-an-issue) with the right information to allow us to address it.
+```sh
+GO111MODULE=on go get golang.org/x/tools/gopls@latest
+```
 
-If you need to talk to us directly (for instance to file an issue with confidential information in it) you can reach out directly to [@stamblerre] or [@ianthehat].
+**NOTE**: Do not use the `-u` flag, as it will update your dependencies to
+incompatible versions.
 
-## More information
+Learn more in the [advanced installation
+instructions](doc/advanced.md#installing-unreleased-versions).
 
-If you want to know more about it, have an unusual use case, or want to contribute, please read the following documents
+## Setting up your workspace
 
-* [Using gopls](doc/user.md)
-* [Troubleshooting and reporting issues](doc/troubleshooting.md)
-* [Integrating gopls with an editor](doc/integrating.md)
-* [Contributing to gopls](doc/contributing.md)
-* [Design requirements and decisions](doc/design.md)
-* [Implementation details](doc/implementation.md)
+`gopls` supports both Go module and GOPATH modes, but if you are working with
+multiple modules or uncommon project layouts, you will need to specifically
+configure your workspace. See the [Workspace document](doc/workspace.md) for
+information on supported workspace layouts.
+
+## Configuration
+
+You can configure `gopls` to change your editor experience or view additional
+debugging information. Configuration options will be made available by your
+editor, so see your [editor's instructions](#editors) for specific details. A
+full list of `gopls` settings can be found in the [Settings documentation](doc/settings.md).
+
+### Environment variables
+
+`gopls` inherits your editor's environment, so be aware of any environment
+variables you configure. Some editors, such as VS Code, allow users to
+selectively override the values of some environment variables.
+
+## Troubleshooting
+
+If you are having issues with `gopls`, please follow the steps described in the
+[troubleshooting guide](doc/troubleshooting.md).
+
+## Supported Go versions and build systems
+
+`gopls` follows the
+[Go Release Policy](https://golang.org/doc/devel/release.html#policy),
+meaning that it officially supports the last 2 major Go releases. Though we
+try not to break older versions, we do not prioritize issues only affecting
+legacy Go releases.
+
+`gopls` currently only supports the `go` command, so if you are using a
+different build system, `gopls` will not work well. Bazel support is currently
+blocked on
+[bazelbuild/rules_go#512](https://github.com/bazelbuild/rules_go/issues/512).
+
+## Additional information
+
+* [Features](doc/features.md)
+* [Command-line interface](doc/command-line.md)
+* [Advanced topics](doc/advanced.md)
+* [Contributing to `gopls`](doc/contributing.md)
+* [Integrating `gopls` with an editor](doc/design/integrating.md)
+* [Design requirements and decisions](doc/design/design.md)
+* [Implementation details](doc/design/implementation.md)
+* [Open issues](https://github.com/golang/go/issues?q=is%3Aissue+is%3Aopen+label%3Agopls)
 
 [language server]: https://langserver.org
-[mailing list]: https://groups.google.com/forum/#!forum/golang-tools
-[slack channel]: https://gophers.slack.com/messages/CJZH85XCZ
-[@stamblerre]: https://github.com/stamblerre "Rebecca Stambler"
-[@ianthehat]: https://github.com/ianthehat "Ian Cottrell"
-[1.0 milestone]: https://github.com/golang/go/milestone/112
-[gopls unplanned]: https://github.com/golang/go/milestone/124
+[LSP]: https://microsoft.github.io/language-server-protocol/
+[Gophers Slack]: https://gophers.slack.com/
diff --git a/gopls/doc/acme.md b/gopls/doc/acme.md
deleted file mode 100644
index e62a028..0000000
--- a/gopls/doc/acme.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Acme
-
-Use the experimental [`acme-lsp`] plugin.
-Get started by following the[installation guide].
-
-[`acme-lsp`]: https://github.com/fhs/acme-lsp
-[installation guide]: https://github.com/fhs/acme-lsp#gopls
diff --git a/gopls/doc/advanced.md b/gopls/doc/advanced.md
new file mode 100644
index 0000000..93c6b8f
--- /dev/null
+++ b/gopls/doc/advanced.md
@@ -0,0 +1,37 @@
+# Advanced topics
+
+This documentation is for advanced `gopls` users, who may want to test
+unreleased versions or try out special features.
+
+## Installing unreleased versions
+
+To get a specific version of `gopls` (for example, to test a prerelease
+version), run:
+
+```sh
+GO111MODULE=on go get golang.org/x/tools/gopls@vX.Y.Z
+```
+
+Where `vX.Y.Z` is the desired version.
+
+### Unstable versions
+
+To update `gopls` to the latest **unstable** version, use:
+
+```sh
+GO111MODULE=on go get golang.org/x/tools/gopls@master golang.org/x/tools@master
+```
+
+## Working on the Go source distribution
+
+If you are working on the [Go project] itself, the `go` command that `gopls`
+invokes will have to correspond to the version of the source you are working
+on. That is, if you have checked out the Go project to `$HOME/go`, your `go`
+command should be the `$HOME/go/bin/go` executable that you built with
+`make.bash` or equivalent.
+
+You can achieve this by adding the right version of `go` to your `PATH`
+(`export PATH=$HOME/go/bin:$PATH` on Unix systems) or by configuring your
+editor.
+
+[Go project]: https://go.googlesource.com/go
diff --git a/gopls/doc/atom.md b/gopls/doc/atom.md
deleted file mode 100644
index ce1d094..0000000
--- a/gopls/doc/atom.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Atom
-
-Use the [`ide-gopls`] package.
-You will also need to install the [`atom-ide-ui`] package.
-
-[`ide-gopls`]: https://github.com/MordFustang21/ide-gopls
-[`atom-ide-ui`]: https://github.com/facebookarchive/atom-ide-ui
diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md
index f545551..0fa1e86 100644
--- a/gopls/doc/commands.md
+++ b/gopls/doc/commands.md
@@ -53,6 +53,12 @@
 go_get_package runs `go get` to fetch a package.
 
 
+### **Check for upgrades**
+Identifier: `gopls.check_upgrades`
+
+check_upgrades checks for module upgrades.
+
+
 ### **Add dependency**
 Identifier: `gopls.add_dependency`
 
diff --git a/gopls/doc/contributing.md b/gopls/doc/contributing.md
index b285a30..a99dc6e 100644
--- a/gopls/doc/contributing.md
+++ b/gopls/doc/contributing.md
@@ -1,7 +1,7 @@
 # Documentation for contributors
 
 This documentation augments the general documentation for contributing to the
-x/tools repository, described at the [repository root](../CONTRIBUTING.md).
+x/tools repository, described at the [repository root](../../CONTRIBUTING.md).
 
 Contributions are welcome, but since development is so active, we request that
 you file an issue and claim it before starting to work on something. Otherwise,
@@ -96,7 +96,7 @@
 Jenkins-like Google infrastructure for running Dockerized tests. This allows us
 to run gopls tests in various environments that would be difficult to add to
 the TryBots. Notably, Kokoro runs tests on
-[older Go versions](user.md#supported-go-versions) that are no longer supported
+[older Go versions](../README.md#supported-go-versions) that are no longer supported
 by the TryBots.
 
 ## Debugging
diff --git a/gopls/doc/daemon.md b/gopls/doc/daemon.md
index ea9c4e2..f54099c 100644
--- a/gopls/doc/daemon.md
+++ b/gopls/doc/daemon.md
@@ -150,10 +150,10 @@
 
 **Q: Why am I not saving as much memory as I expected when using a shared gopls?**
 
-A: As described in [implementation.md](implementation.md), gopls has a concept
-of view/session/cache. Each session and view map onto exactly one editor
-session (because they contain things like edited but unsaved buffers). The
-cache contains things that are independent of any editor session, and can
+A: As described in [implementation.md](design/implementation.md), gopls has a
+concept of view/session/cache. Each session and view map onto exactly one
+editor session (because they contain things like edited but unsaved buffers).
+The cache contains things that are independent of any editor session, and can
 therefore be shared.
 
 When, for example, three editor session are sharing a single gopls process,
diff --git a/gopls/doc/emacs.md b/gopls/doc/emacs.md
index 707dd90..471dbf1 100644
--- a/gopls/doc/emacs.md
+++ b/gopls/doc/emacs.md
@@ -3,7 +3,7 @@
 ## Installing `gopls`
 
 To use `gopls` with Emacs, you must first
-[install the `gopls` binary](user.md#installation) and ensure that the directory
+[install the `gopls` binary](../README.md#installation) and ensure that the directory
 containing the resulting binary (either `$(go env GOBIN)` or `$(go env
 GOPATH)/bin`) is in your `PATH`.
 
diff --git a/gopls/doc/troubleshooting.md b/gopls/doc/troubleshooting.md
index 58e5fa4..121dd86 100644
--- a/gopls/doc/troubleshooting.md
+++ b/gopls/doc/troubleshooting.md
@@ -10,7 +10,7 @@
 
 1. Verify that your project is in good shape by working with it outside of your editor. Running a command like `go build ./...` in the workspace directory will compile everything. For modules, `go mod tidy` is another good check, though it may modify your `go.mod`.
 1. Check that your editor isn't showing any diagnostics that indicate a problem with your workspace. They may appear as diagnostics on a Go file's package declaration, diagnostics in a go.mod file, or as a status or progress message. Problems in the workspace configuration can cause many different symptoms. See the [workspace setup instructions](workspace.md) for help.
-1. Make sure `gopls` is up to date by following the [installation instructions](user.md#installing), then [restarting gopls](#restart-gopls).
+1. Make sure `gopls` is up to date by following the [installation instructions](../README.md#installation), then [restarting gopls](#restart-gopls).
 1. Optionally, [ask for help](#ask-for-help) on Gophers Slack.
 1. Finally, [report the issue](#file-an-issue) to the `gopls` developers.
 
@@ -41,7 +41,7 @@
 
 To increase the level of detail in your logs, start `gopls` with the `-rpc.trace` flag. To start a debug server that will allow you to see profiles and memory usage, start `gopls` with `serve --debug=localhost:6060`. You will then be able to view debug information by navigating to `localhost:6060`.
 
-If you are unsure of how to pass a flag to `gopls` through your editor, please see the [documentation for your editor](user.md#editors).
+If you are unsure of how to pass a flag to `gopls` through your editor, please see the [documentation for your editor](../README.md#editors).
 
 ## Debug memory usage
 
diff --git a/gopls/doc/user.md b/gopls/doc/user.md
deleted file mode 100644
index fce4b9b..0000000
--- a/gopls/doc/user.md
+++ /dev/null
@@ -1,165 +0,0 @@
-# User guide
-
-**If you're having issues with `gopls`, please see the
-[troubleshooting guide](troubleshooting.md).**
-
-## Editors
-
-The following is the list of editors with known integrations for `gopls`.
-
-* [VSCode](vscode.md)
-* [Vim / Neovim](vim.md)
-* [Emacs](emacs.md)
-* [Acme](acme.md)
-* [Sublime Text](subl.md)
-* [Atom](atom.md)
-
-If you use `gopls` with an editor that is not on this list, please let us know
-by [filing an issue](#new-issue) or [modifying this documentation](contributing.md).
-
-## Overview
-
-* [Installation](#installation)
-* [Configuration](#configuration)
-
-Learn more at the following pages:
-
-* [Features](features.md)
-* [Command-line](command-line.md)
-
-## Installation
-
-For the most part, you should not need to install or update `gopls`. Your editor should handle that step for you.
-
-If you do want to get the latest stable version of `gopls`, change to any directory that is both outside of your `GOPATH` and outside of a module (a temp directory is fine), and run
-
-```sh
-go get golang.org/x/tools/gopls@latest
-```
-
-**Do not** use the `-u` flag, as it will update your dependencies to incompatible versions.
-
-To get a specific version of `gopls` (for example, to test a prerelease
-version), run:
-
-```sh
-go get golang.org/x/tools/gopls@vX.Y.Z
-```
-
-Where `vX.Y.Z` is the desired version.
-
-If you see this error:
-
-```sh
-$ go get golang.org/x/tools/gopls@latest
-go: cannot use path@version syntax in GOPATH mode
-```
-
-then run
-
-```sh
-GO111MODULE=on go get golang.org/x/tools/gopls@latest
-```
-
-### Unstable versions
-
-`go get` doesn't honor the `replace` directive in the `go.mod` of
-`gopls` when you are outside of the `gopls` module, so a simple `go get`
-with `@master` could fail.  To actually update your `gopls` to the
-latest **unstable** version, use:
-
-```sh
-go get golang.org/x/tools/gopls@master golang.org/x/tools@master
-```
-
-In general, you should use `@latest` instead, to prevent frequent
-breakages.
-
-### Supported Go versions
-
-`gopls` follows the
-[Go Release Policy](https://golang.org/doc/devel/release.html#policy),
-meaning that it officially supports the last 2 major Go releases. We run CI to
-verify that the `gopls` tests pass for the last 4 major Go releases, but do not
-prioritize issues only affecting legacy Go release (3 or 4 releases ago).
-
-## Configuration
-
-### Environment variables
-
-These are often inherited from the editor that launches `gopls`, and sometimes
-the editor has a way to add or replace values before launching. For example,
-VSCode allows you to configure `go.toolsEnvVars`.
-
-Configuring your environment correctly is important, as `gopls` relies on the
-`go` command.
-
-### Command-line flags
-
-See the [command-line page](command-line.md) for more information about the
-flags you might specify. All editors support some way of adding flags to
-`gopls`, for the most part you should not need to do this unless you have very
-unusual requirements or are trying to [troubleshoot](troubleshooting.md#steps)
-`gopls` behavior.
-
-### Editor settings
-
-For the most part these will be settings that control how the editor interacts
-with or uses the results of `gopls`, not modifications to `gopls` itself. This
-means they are not standardized across editors, and you will have to look at
-the specific instructions for your editor integration to change them.
-
-#### The set of workspace folders
-
-This is one of the most important pieces of configuration. It is the set of
-folders that gopls considers to be "roots" that it should consider files to
-be a part of.
-
-If you are using modules there should be one of these per go.mod that you
-are working on. If you do not open the right folders, very little will work.
-**This is the most common misconfiguration of `gopls` that we see**.
-
-#### Global configuration
-
-There should be a way of declaring global settings for `gopls` inside the
-editor. The settings block will be called `"gopls"` and contains a collection
-of controls for `gopls` that the editor is not expected to understand or
-control.
-
-In VSCode, this would be a section in your settings file that might look like
-this:
-
-```json5
-  "gopls": {
-    "usePlaceholders": true,
-    "completeUnimported": true
-  },
-```
-
-See [Settings](settings.md) for more information about the available
-configurations.
-
-#### Workspace folder configuration
-
-This contains exactly the same set of values that are in the global
-configuration, but it is fetched for every workspace folder separately.
-The editor can choose to respond with different values per-folder.
-
-### Working on the Go source distribution
-
-If you are working on the [Go project](https://go.googlesource.com/go) itself,
-your `go` command will have to correspond to the version of the source you are
-working on. That is, if you have downloaded the code to `$HOME/go`, your `go`
-command should be the `$HOME/go/bin/go` executable that you built with
-`make.bash` or equivalent.
-
-You can achieve this by adding the right version of `go` to your `PATH` (`export PATH=$HOME/go/bin:$PATH` on Unix systems) or by configuring your editor. In VS Code, you can use the `go.alternateTools` setting to point to the correct version of `go`:
-
-```json5
-{
-
-    "go.alternateTools": {
-        "go": "$HOME/bin/go"
-    }
-}
-```
diff --git a/gopls/doc/vscode.md b/gopls/doc/vscode.md
deleted file mode 100644
index 11f8efa..0000000
--- a/gopls/doc/vscode.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# VS Code
-
-Use the [VS Code Go] plugin, with the following configuration:
-
-```json5
-"go.useLanguageServer": true,
-```
-
-As of February 2020, `gopls` will be enabled by default in [VS Code Go].
-To learn more, follow along with
-[golang.vscode-go#1037](https://github.com/golang/vscode-go/issues/1037).
-
-```json5
-"gopls": {
-    // Add parameter placeholders when completing a function.
-    "ui.completion.usePlaceholders": true,
-
-    // If true, enable additional analyses with staticcheck.
-    // Warning: This will significantly increase memory usage.
-    "ui.diagnostic.staticcheck": false,
-    
-    // For more customization, see
-    // see https://github.com/golang/vscode-go/blob/master/docs/settings.md.
-}
-```
-
-To enable more detailed debug information, add the following to your VSCode settings:
-
-```json5
-"go.languageServerFlags": [
-    "-rpc.trace", // for more detailed debug logging
-    "serve",
-    "--debug=localhost:6060", // Optional: investigate memory usage, see profiles
-],
-```
-
-See the section on [command line](command-line.md) arguments for more
-information about what these do, along with other things like
-`--logfile=auto` that you might want to use.
-
-## Build tags and flags
-
-Build tags and flags will be automatically picked up from `"go.buildTags"` and
-`"go.buildFlags"` settings. In the rare case that you don't want that default
-behavior, you can still override the settings from the `gopls` section, using
-`"gopls": { "build.buildFlags": [] }`.
-
-## Remote Development with `gopls`
-
-You can also make use of `gopls` with the
-[VS Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview)
-extensions to enable full-featured Go development on a lightweight client
-machine, while connected to a more powerful server machine.
-
-First, install the Remote Development extension of your choice, such as the
-[Remote - SSH](https://code.visualstudio.com/docs/remote/ssh) extension. Once
-you open a remote session in a new window, open the Extensions pane
-(Ctrl+Shift+X) and you will see several different sections listed. In the
-"Local - Installed" section, navigate to the Go extension and click
-"Install in SSH: hostname".
-
-Once you have reloaded VS Code, you will be prompted to install `gopls` and other
-Go-related tools. After one more reload, you should be ready to develop remotely
-with VS Code and the Go extension.
-
-[VS Code Go]: https://github.com/golang/vscode-go
diff --git a/gopls/doc/workspace.md b/gopls/doc/workspace.md
index 6f56bf5..ed30dae 100644
--- a/gopls/doc/workspace.md
+++ b/gopls/doc/workspace.md
@@ -19,16 +19,16 @@
 
 ### Multiple modules
 
-As of Jan 2020, if you are working with multiple modules, you will need to
-create a "workspace folder" for each module. This means that each module has
-its own scope, and features will not work across modules. We are currently
-working on addressing this limitation--see details about
+As of Jan 2021, if you are working with multiple modules or nested modules, you
+will need to create a "workspace folder" for each module. This means that each
+module has its own scope, and features will not work across modules. We are
+currently working on addressing this limitation--see details about
 [experimental workspace module mode](#experimental-workspace-module-mode)
 below.
 
 In VS Code, you can create a workspace folder by setting up a
 [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces).
-View the [documentation for your editor plugin](user.md#editor) to learn how to
+View the [documentation for your editor plugin](../README.md#editor) to learn how to
 configure a workspace folder in your editor.
 
 #### Workspace module (experimental)
diff --git a/gopls/go.mod b/gopls/go.mod
index 8bfb4d3..a8f4e80 100644
--- a/gopls/go.mod
+++ b/gopls/go.mod
@@ -14,3 +14,5 @@
 	mvdan.cc/gofumpt v0.1.0
 	mvdan.cc/xurls/v2 v2.2.0
 )
+
+replace golang.org/x/tools => ../
diff --git a/gopls/internal/hooks/gen-licenses.sh b/gopls/internal/hooks/gen-licenses.sh
index f0756ff..7d6bab7 100755
--- a/gopls/internal/hooks/gen-licenses.sh
+++ b/gopls/internal/hooks/gen-licenses.sh
@@ -6,13 +6,10 @@
 
 set -o pipefail
 
+output=$1
 tempfile=$(mktemp)
 cd $(dirname $0)
 
-modhash=$(sha256sum ../../go.sum | awk '{print $1}')
-# Make sure we have the code for all the modules we depend on.
-go mod download
-
 cat > $tempfile <<END
 // Copyright 2020 The Go Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style
@@ -37,9 +34,5 @@
   echo >> $tempfile
 done
 
-cat >> $tempfile << END
-\`
-
-const licensesGeneratedFrom = "$modhash"
-END
-mv $tempfile licenses.go
\ No newline at end of file
+echo "\`" >> $tempfile
+mv $tempfile $output
\ No newline at end of file
diff --git a/gopls/internal/hooks/licenses.go b/gopls/internal/hooks/licenses.go
index ac5bdbb..a159465 100644
--- a/gopls/internal/hooks/licenses.go
+++ b/gopls/internal/hooks/licenses.go
@@ -167,5 +167,3 @@
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 `
-
-const licensesGeneratedFrom = "2f7edcd0817bc065239a9c855c8ca7e49dd04cc18b33db2a3149472e4ffaa829"
diff --git a/gopls/internal/hooks/licenses_test.go b/gopls/internal/hooks/licenses_test.go
index 28b1149..d2c4e34 100644
--- a/gopls/internal/hooks/licenses_test.go
+++ b/gopls/internal/hooks/licenses_test.go
@@ -5,19 +5,36 @@
 package hooks
 
 import (
-	"crypto/sha256"
-	"encoding/hex"
+	"bytes"
 	"io/ioutil"
+	"os/exec"
+	"runtime"
 	"testing"
 )
 
 func TestLicenses(t *testing.T) {
-	sumBytes, err := ioutil.ReadFile("../../go.sum")
+	if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
+		t.Skip("generating licenses only works on Unixes")
+	}
+	tmp, err := ioutil.TempFile("", "")
 	if err != nil {
 		t.Fatal(err)
 	}
-	sumSum := sha256.Sum256(sumBytes)
-	if licensesGeneratedFrom != hex.EncodeToString(sumSum[:]) {
+	tmp.Close()
+
+	if out, err := exec.Command("./gen-licenses.sh", tmp.Name()).CombinedOutput(); err != nil {
+		t.Fatalf("generating licenses failed: %q, %v", out, err)
+	}
+
+	got, err := ioutil.ReadFile(tmp.Name())
+	if err != nil {
+		t.Fatal(err)
+	}
+	want, err := ioutil.ReadFile("licenses.go")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !bytes.Equal(got, want) {
 		t.Error("combined license text needs updating. Run: `go generate ./internal/hooks` from the gopls module.")
 	}
 }
diff --git a/gopls/internal/regtest/bench_test.go b/gopls/internal/regtest/bench/bench_test.go
similarity index 92%
rename from gopls/internal/regtest/bench_test.go
rename to gopls/internal/regtest/bench/bench_test.go
index 5ad9358..1702e84 100644
--- a/gopls/internal/regtest/bench_test.go
+++ b/gopls/internal/regtest/bench/bench_test.go
@@ -2,16 +2,22 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package bench
 
 import (
 	"flag"
 	"fmt"
 	"testing"
 
+	. "golang.org/x/tools/gopls/internal/regtest"
+
 	"golang.org/x/tools/internal/lsp/protocol"
 )
 
+func TestMain(m *testing.M) {
+	Main(m)
+}
+
 func printBenchmarkResults(result testing.BenchmarkResult) {
 	fmt.Println("Benchmark Statistics:")
 	fmt.Println(result.String())
@@ -37,7 +43,7 @@
 
 	results := testing.Benchmark(func(b *testing.B) {
 		for i := 0; i < b.N; i++ {
-			withOptions(opts...).run(t, "", func(t *testing.T, env *Env) {})
+			WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) {})
 		}
 	})
 
@@ -72,7 +78,7 @@
 	}
 	opts = append(opts, conf)
 
-	withOptions(opts...).run(t, "", func(t *testing.T, env *Env) {
+	WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) {
 		// We can't Await in this test, since we have disabled hooks. Instead, run
 		// one symbol request to completion to ensure all necessary cache entries
 		// are populated.
diff --git a/gopls/internal/regtest/completion_bench_test.go b/gopls/internal/regtest/bench/completion_bench_test.go
similarity index 97%
rename from gopls/internal/regtest/completion_bench_test.go
rename to gopls/internal/regtest/bench/completion_bench_test.go
index 267eeb6..be36d45 100644
--- a/gopls/internal/regtest/completion_bench_test.go
+++ b/gopls/internal/regtest/bench/completion_bench_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package bench
 
 import (
 	"flag"
@@ -11,6 +11,8 @@
 	"strings"
 	"testing"
 
+	. "golang.org/x/tools/gopls/internal/regtest"
+
 	"golang.org/x/tools/internal/lsp/fake"
 )
 
@@ -45,7 +47,7 @@
 	// it first (and therefore need hooks).
 	opts = append(opts, SkipHooks(false))
 
-	withOptions(opts...).run(t, "", func(t *testing.T, env *Env) {
+	WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) {
 		env.OpenFile(options.file)
 
 		// Run edits required for this completion.
diff --git a/gopls/internal/regtest/stress_test.go b/gopls/internal/regtest/bench/stress_test.go
similarity index 94%
rename from gopls/internal/regtest/stress_test.go
rename to gopls/internal/regtest/bench/stress_test.go
index 77f4ff2..8cdbcfe 100644
--- a/gopls/internal/regtest/stress_test.go
+++ b/gopls/internal/regtest/bench/stress_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package bench
 
 import (
 	"context"
@@ -10,6 +10,8 @@
 	"fmt"
 	"testing"
 	"time"
+
+	. "golang.org/x/tools/gopls/internal/regtest"
 )
 
 // Pilosa is a repository that has historically caused significant memory
@@ -50,7 +52,7 @@
 	}
 	opts := stressTestOptions(*pilosaPath)
 
-	withOptions(opts...).run(t, "", func(t *testing.T, env *Env) {
+	WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) {
 		files := []string{
 			"cmd.go",
 			"internal/private.pb.go",
diff --git a/gopls/internal/regtest/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go
similarity index 83%
rename from gopls/internal/regtest/codelens_test.go
rename to gopls/internal/regtest/codelens/codelens_test.go
index a58de79..52a7e11 100644
--- a/gopls/internal/regtest/codelens_test.go
+++ b/gopls/internal/regtest/codelens/codelens_test.go
@@ -2,14 +2,15 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package codelens
 
 import (
 	"runtime"
 	"strings"
 	"testing"
 
-	"golang.org/x/tools/internal/lsp"
+	. "golang.org/x/tools/gopls/internal/regtest"
+
 	"golang.org/x/tools/internal/lsp/fake"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
@@ -17,6 +18,10 @@
 	"golang.org/x/tools/internal/testenv"
 )
 
+func TestMain(m *testing.M) {
+	Main(m)
+}
+
 func TestDisablingCodeLens(t *testing.T) {
 	const workspace = `
 -- go.mod --
@@ -53,11 +58,11 @@
 	}
 	for _, test := range tests {
 		t.Run(test.label, func(t *testing.T) {
-			withOptions(
+			WithOptions(
 				EditorConfig{
 					CodeLenses: test.enabled,
 				},
-			).run(t, workspace, func(t *testing.T, env *Env) {
+			).Run(t, workspace, func(t *testing.T, env *Env) {
 				env.OpenFile("lib.go")
 				lens := env.CodeLens("lib.go")
 				if gotCodeLens := len(lens) > 0; gotCodeLens != test.wantCodeLens {
@@ -111,14 +116,22 @@
 	_ = hi.Goodbye
 }
 `
+
+	const wantGoMod = `module mod.com
+
+go 1.12
+
+require golang.org/x/hello v1.3.3
+`
+
 	for _, commandTitle := range []string{
 		"Upgrade transitive dependencies",
 		"Upgrade direct dependencies",
 	} {
 		t.Run(commandTitle, func(t *testing.T) {
-			withOptions(
+			WithOptions(
 				ProxyFiles(proxyWithLatest),
-			).run(t, shouldUpdateDep, func(t *testing.T, env *Env) {
+			).Run(t, shouldUpdateDep, func(t *testing.T, env *Env) {
 				env.OpenFile("go.mod")
 				var lens protocol.CodeLens
 				var found bool
@@ -137,20 +150,27 @@
 				}); err != nil {
 					t.Fatal(err)
 				}
-				env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1))
-				got := env.Editor.BufferText("go.mod")
-				const wantGoMod = `module mod.com
-
-go 1.12
-
-require golang.org/x/hello v1.3.3
-`
-				if got != wantGoMod {
+				env.Await(env.DoneWithChangeWatchedFiles())
+				if got := env.Editor.BufferText("go.mod"); got != wantGoMod {
 					t.Fatalf("go.mod upgrade failed:\n%s", tests.Diff(t, wantGoMod, got))
 				}
 			})
 		})
 	}
+	t.Run("Upgrade individual dependency", func(t *testing.T) {
+		WithOptions(ProxyFiles(proxyWithLatest)).Run(t, shouldUpdateDep, func(t *testing.T, env *Env) {
+			env.OpenFile("go.mod")
+			env.ExecuteCodeLensCommand("go.mod", source.CommandCheckUpgrades)
+			d := &protocol.PublishDiagnosticsParams{}
+			env.Await(OnceMet(env.DiagnosticAtRegexpWithMessage("go.mod", `require`, "can be upgraded"),
+				ReadDiagnostics("go.mod", d)))
+			env.ApplyQuickFixes("go.mod", d.Diagnostics)
+			env.Await(env.DoneWithChangeWatchedFiles())
+			if got := env.Editor.BufferText("go.mod"); got != wantGoMod {
+				t.Fatalf("go.mod upgrade failed:\n%s", tests.Diff(t, wantGoMod, got))
+			}
+		})
+	})
 }
 
 func TestUnusedDependenciesCodelens(t *testing.T) {
@@ -196,10 +216,10 @@
 	_ = hi.Goodbye
 }
 `
-	runner.Run(t, shouldRemoveDep, func(t *testing.T, env *Env) {
+	WithOptions(ProxyFiles(proxy)).Run(t, shouldRemoveDep, func(t *testing.T, env *Env) {
 		env.OpenFile("go.mod")
 		env.ExecuteCodeLensCommand("go.mod", source.CommandTidy)
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1))
+		env.Await(env.DoneWithChangeWatchedFiles())
 		got := env.Editor.BufferText("go.mod")
 		const wantGoMod = `module mod.com
 
@@ -210,7 +230,7 @@
 		if got != wantGoMod {
 			t.Fatalf("go.mod tidy failed:\n%s", tests.Diff(t, wantGoMod, got))
 		}
-	}, ProxyFiles(proxy))
+	})
 }
 
 func TestRegenerateCgo(t *testing.T) {
@@ -234,7 +254,7 @@
 	print(C.fortytwo())
 }
 `
-	runner.Run(t, workspace, func(t *testing.T, env *Env) {
+	Run(t, workspace, func(t *testing.T, env *Env) {
 		// Open the file. We should have a nonexistant symbol.
 		env.OpenFile("cgo.go")
 		env.Await(env.DiagnosticAtRegexp("cgo.go", `C\.(fortytwo)`)) // could not determine kind of name for C.fortytwo
@@ -271,12 +291,12 @@
 	fmt.Println(x)
 }
 `
-	withOptions(
+	WithOptions(
 		EditorConfig{
 			CodeLenses: map[string]bool{
 				"gc_details": true,
 			}},
-	).run(t, mod, func(t *testing.T, env *Env) {
+	).Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		env.ExecuteCodeLensCommand("main.go", source.CommandToggleDetails)
 		d := &protocol.PublishDiagnosticsParams{}
diff --git a/gopls/internal/regtest/completion_test.go b/gopls/internal/regtest/completion/completion_test.go
similarity index 91%
rename from gopls/internal/regtest/completion_test.go
rename to gopls/internal/regtest/completion/completion_test.go
index 412fdab..7b8f966 100644
--- a/gopls/internal/regtest/completion_test.go
+++ b/gopls/internal/regtest/completion/completion_test.go
@@ -2,19 +2,43 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package completion
 
 import (
 	"fmt"
 	"strings"
 	"testing"
 
-	"golang.org/x/tools/internal/lsp"
+	. "golang.org/x/tools/gopls/internal/regtest"
+
 	"golang.org/x/tools/internal/lsp/fake"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/testenv"
 )
 
+func TestMain(m *testing.M) {
+	Main(m)
+}
+
+const proxy = `
+-- example.com@v1.2.3/go.mod --
+module example.com
+
+go 1.12
+-- example.com@v1.2.3/blah/blah.go --
+package blah
+
+const Name = "Blah"
+-- random.org@v1.2.3/go.mod --
+module random.org
+
+go 1.12
+-- random.org@v1.2.3/blah/blah.go --
+package hello
+
+const Name = "Hello"
+`
+
 func TestPackageCompletion(t *testing.T) {
 	testenv.NeedsGo1Point(t, 14)
 	const files = `
@@ -124,11 +148,11 @@
 		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			run(t, files, func(t *testing.T, env *Env) {
+			Run(t, files, func(t *testing.T, env *Env) {
 				if tc.content != nil {
 					env.WriteWorkspaceFile(tc.filename, *tc.content)
 					env.Await(
-						CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+						env.DoneWithChangeWatchedFiles(),
 					)
 				}
 				env.OpenFile(tc.filename)
@@ -181,7 +205,7 @@
 `
 
 	want := []string{"ma", "ma_test", "main", "math", "math_test"}
-	run(t, files, func(t *testing.T, env *Env) {
+	Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("math/add.go")
 		completions := env.Completion("math/add.go", fake.Pos{
 			Line:   0,
@@ -242,31 +266,29 @@
 	_ = blah.Hello
 }
 `
-	withOptions(
+	WithOptions(
 		ProxyFiles(proxy),
-	).run(t, mod, func(t *testing.T, env *Env) {
+	).Run(t, mod, func(t *testing.T, env *Env) {
 		// Make sure the dependency is in the module cache and accessible for
 		// unimported completions, and then remove it before proceeding.
 		env.RemoveWorkspaceFile("main2.go")
 		env.RunGoCommand("mod", "tidy")
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2))
+		env.Await(env.DoneWithChangeWatchedFiles())
 
 		// Trigger unimported completions for the example.com/blah package.
 		env.OpenFile("main.go")
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1))
+		env.Await(env.DoneWithOpen())
 		pos := env.RegexpSearch("main.go", "ah")
 		completions := env.Completion("main.go", pos)
 		if len(completions.Items) == 0 {
 			t.Fatalf("no completion items")
 		}
 		env.AcceptCompletion("main.go", pos, completions.Items[0])
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1))
+		env.Await(env.DoneWithChange())
 
 		// Trigger completions once again for the blah.<> selector.
 		env.RegexpReplace("main.go", "_ = blah", "_ = blah.")
-		env.Await(
-			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 2),
-		)
+		env.Await(env.DoneWithChange())
 		pos = env.RegexpSearch("main.go", "\n}")
 		completions = env.Completion("main.go", pos)
 		if len(completions.Items) != 1 {
@@ -313,7 +335,7 @@
 
 const Name = "mainmod"
 `
-	withOptions(ProxyFiles(proxy)).run(t, files, func(t *testing.T, env *Env) {
+	WithOptions(ProxyFiles(proxy)).Run(t, files, func(t *testing.T, env *Env) {
 		env.CreateBuffer("import.go", "package pkg\nvar _ = mainmod.Name\n")
 		env.SaveBuffer("import.go")
 		content := env.ReadWorkspaceFile("import.go")
diff --git a/gopls/internal/regtest/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go
similarity index 85%
rename from gopls/internal/regtest/diagnostics_test.go
rename to gopls/internal/regtest/diagnostics/diagnostics_test.go
index 8aa45ca..50ea0b2 100644
--- a/gopls/internal/regtest/diagnostics_test.go
+++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package diagnostics
 
 import (
 	"context"
@@ -11,6 +11,8 @@
 	"os"
 	"testing"
 
+	. "golang.org/x/tools/gopls/internal/regtest"
+
 	"golang.org/x/tools/internal/lsp"
 	"golang.org/x/tools/internal/lsp/fake"
 	"golang.org/x/tools/internal/lsp/protocol"
@@ -18,6 +20,10 @@
 	"golang.org/x/tools/internal/testenv"
 )
 
+func TestMain(m *testing.M) {
+	Main(m)
+}
+
 // Use mod.com for all go.mod files due to golang/go#35230.
 const exampleProgram = `
 -- go.mod --
@@ -37,7 +43,7 @@
 	// This test is very basic: start with a clean Go program, make an error, and
 	// get a diagnostic for that error. However, it also demonstrates how to
 	// combine Expectations to await more complex state in the editor.
-	runner.Run(t, exampleProgram, func(t *testing.T, env *Env) {
+	Run(t, exampleProgram, func(t *testing.T, env *Env) {
 		// Deleting the 'n' at the end of Println should generate a single error
 		// diagnostic.
 		env.OpenFile("main.go")
@@ -46,7 +52,7 @@
 			// Once we have gotten diagnostics for the change above, we should
 			// satisfy the DiagnosticAtRegexp assertion.
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1),
+				env.DoneWithChange(),
 				env.DiagnosticAtRegexp("main.go", "Printl"),
 			),
 			// Assert that this test has sent no error logs to the client. This is not
@@ -65,7 +71,7 @@
 
 go 1.12
 `
-	runner.Run(t, onlyMod, func(t *testing.T, env *Env) {
+	Run(t, onlyMod, func(t *testing.T, env *Env) {
 		env.CreateBuffer("main.go", `package main
 
 func m() {
@@ -87,7 +93,7 @@
 
 const Foo = "abc
 `
-	runner.Run(t, brokenFile, func(t *testing.T, env *Env) {
+	Run(t, brokenFile, func(t *testing.T, env *Env) {
 		env.CreateBuffer("broken.go", brokenFile)
 		env.Await(env.DiagnosticAtRegexp("broken.go", "\"abc"))
 	})
@@ -110,7 +116,7 @@
 `
 
 func TestDiagnosticClearingOnEdit(t *testing.T) {
-	runner.Run(t, badPackage, func(t *testing.T, env *Env) {
+	Run(t, badPackage, func(t *testing.T, env *Env) {
 		env.OpenFile("b.go")
 		env.Await(env.DiagnosticAtRegexp("a.go", "a = 1"), env.DiagnosticAtRegexp("b.go", "a = 2"))
 
@@ -124,7 +130,7 @@
 }
 
 func TestDiagnosticClearingOnDelete_Issue37049(t *testing.T) {
-	runner.Run(t, badPackage, func(t *testing.T, env *Env) {
+	Run(t, badPackage, func(t *testing.T, env *Env) {
 		env.OpenFile("a.go")
 		env.Await(env.DiagnosticAtRegexp("a.go", "a = 1"), env.DiagnosticAtRegexp("b.go", "a = 2"))
 		env.RemoveWorkspaceFile("b.go")
@@ -134,7 +140,7 @@
 }
 
 func TestDiagnosticClearingOnClose(t *testing.T) {
-	runner.Run(t, badPackage, func(t *testing.T, env *Env) {
+	Run(t, badPackage, func(t *testing.T, env *Env) {
 		env.CreateBuffer("c.go", `package consts
 
 const a = 3`)
@@ -152,7 +158,7 @@
 
 // Tests golang/go#37978.
 func TestIssue37978(t *testing.T) {
-	runner.Run(t, exampleProgram, func(t *testing.T, env *Env) {
+	Run(t, exampleProgram, func(t *testing.T, env *Env) {
 		// Create a new workspace-level directory and empty file.
 		env.CreateBuffer("c/c.go", "")
 
@@ -203,7 +209,7 @@
 // Tests golang/go#38878: deleting a test file should clear its errors, and
 // not break the workspace.
 func TestDeleteTestVariant(t *testing.T) {
-	runner.Run(t, test38878, func(t *testing.T, env *Env) {
+	Run(t, test38878, func(t *testing.T, env *Env) {
 		env.Await(env.DiagnosticAtRegexp("a_test.go", `f\((3)\)`))
 		env.RemoveWorkspaceFile("a_test.go")
 		env.Await(EmptyDiagnostics("a_test.go"))
@@ -220,12 +226,12 @@
 // should not clear its errors.
 func TestDeleteTestVariant_DiskOnly(t *testing.T) {
 	log.SetFlags(log.Lshortfile)
-	runner.Run(t, test38878, func(t *testing.T, env *Env) {
+	Run(t, test38878, func(t *testing.T, env *Env) {
 		env.OpenFile("a_test.go")
 		env.Await(DiagnosticAt("a_test.go", 5, 3))
 		env.Sandbox.Workdir.RemoveFile(context.Background(), "a_test.go")
 		env.Await(OnceMet(
-			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+			env.DoneWithChangeWatchedFiles(),
 			DiagnosticAt("a_test.go", 5, 3)))
 	})
 }
@@ -251,7 +257,7 @@
 `
 
 	t.Run("manual", func(t *testing.T) {
-		runner.Run(t, noMod, func(t *testing.T, env *Env) {
+		Run(t, noMod, func(t *testing.T, env *Env) {
 			env.Await(
 				env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`),
 			)
@@ -276,7 +282,7 @@
 		})
 	})
 	t.Run("initialized", func(t *testing.T) {
-		runner.Run(t, noMod, func(t *testing.T, env *Env) {
+		Run(t, noMod, func(t *testing.T, env *Env) {
 			env.Await(
 				env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`),
 			)
@@ -289,9 +295,9 @@
 	})
 
 	t.Run("without workspace module", func(t *testing.T) {
-		withOptions(
+		WithOptions(
 			Modes(Singleton),
-		).run(t, noMod, func(t *testing.T, env *Env) {
+		).Run(t, noMod, func(t *testing.T, env *Env) {
 			env.Await(
 				env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`),
 			)
@@ -339,7 +345,7 @@
 }
 `
 
-	runner.Run(t, testPackage, func(t *testing.T, env *Env) {
+	Run(t, testPackage, func(t *testing.T, env *Env) {
 		env.OpenFile("lib_test.go")
 		env.Await(
 			DiagnosticAt("lib_test.go", 10, 2),
@@ -365,7 +371,7 @@
 package foo
 func main() {}
 `
-	runner.Run(t, packageChange, func(t *testing.T, env *Env) {
+	Run(t, packageChange, func(t *testing.T, env *Env) {
 		env.OpenFile("a.go")
 		env.RegexpReplace("a.go", "foo", "foox")
 		env.Await(
@@ -375,7 +381,7 @@
 			// test to actually exercise the bug, we must wait until that work has
 			// completed.
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1),
+				env.DoneWithChange(),
 				NoDiagnostics("a.go"),
 			),
 		)
@@ -418,17 +424,19 @@
 `
 
 func TestResolveDiagnosticWithDownload(t *testing.T) {
-	runner.Run(t, testPackageWithRequire, func(t *testing.T, env *Env) {
+	WithOptions(
+		ProxyFiles(testPackageWithRequireProxy),
+	).Run(t, testPackageWithRequire, func(t *testing.T, env *Env) {
 		env.OpenFile("print.go")
 		// Check that gopackages correctly loaded this dependency. We should get a
 		// diagnostic for the wrong formatting type.
 		// TODO: we should be able to easily also match the diagnostic message.
 		env.Await(env.DiagnosticAtRegexp("print.go", "fmt.Printf"))
-	}, ProxyFiles(testPackageWithRequireProxy))
+	})
 }
 
 func TestMissingDependency(t *testing.T) {
-	runner.Run(t, testPackageWithRequire, func(t *testing.T, env *Env) {
+	Run(t, testPackageWithRequire, func(t *testing.T, env *Env) {
 		env.OpenFile("print.go")
 		env.Await(LogMatching(protocol.Error, "initial workspace load failed", 1))
 	})
@@ -444,7 +452,7 @@
 	var x int
 }
 `
-	runner.Run(t, adHoc, func(t *testing.T, env *Env) {
+	Run(t, adHoc, func(t *testing.T, env *Env) {
 		env.OpenFile("b/b.go")
 		env.Await(env.DiagnosticAtRegexp("b/b.go", "x"))
 	})
@@ -460,13 +468,13 @@
 	fmt.Println("Hello World")
 }
 `
-	withOptions(
+	WithOptions(
 		EditorConfig{
 			Env: map[string]string{
 				"GOPATH":      "",
 				"GO111MODULE": "off",
 			},
-		}).run(t, files, func(t *testing.T, env *Env) {
+		}).Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		env.Await(env.DiagnosticAtRegexp("main.go", "fmt"))
 		env.SaveBuffer("main.go")
@@ -491,7 +499,7 @@
 var X = 0
 `
 	editorConfig := EditorConfig{Env: map[string]string{"GOFLAGS": "-tags=foo"}}
-	withOptions(editorConfig).run(t, files, func(t *testing.T, env *Env) {
+	WithOptions(editorConfig).Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		env.OrganizeImports("main.go")
 		env.Await(EmptyDiagnostics("main.go"))
@@ -516,7 +524,7 @@
 	}
 }
 `
-	runner.Run(t, generated, func(t *testing.T, env *Env) {
+	Run(t, generated, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		original := env.ReadWorkspaceFile("main.go")
 		var d protocol.PublishDiagnosticsParams
@@ -549,7 +557,7 @@
 	hello.Goodbye()
 }
 `
-	runner.Run(t, noModule, func(t *testing.T, env *Env) {
+	Run(t, noModule, func(t *testing.T, env *Env) {
 		env.OpenFile("a.go")
 		env.Await(
 			OutstandingWork(lsp.WorkspaceLoadFailure, "outside of a module"),
@@ -568,13 +576,13 @@
 `
 	for _, go111module := range []string{"on", "off", ""} {
 		t.Run(fmt.Sprintf("GO111MODULE_%v", go111module), func(t *testing.T) {
-			withOptions(EditorConfig{
+			WithOptions(EditorConfig{
 				Env: map[string]string{"GO111MODULE": go111module},
-			}).run(t, files, func(t *testing.T, env *Env) {
+			}).Run(t, files, func(t *testing.T, env *Env) {
 				env.OpenFile("hello.txt")
 				env.Await(
 					OnceMet(
-						CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1),
+						env.DoneWithOpen(),
 						NoShowMessage(),
 					),
 				)
@@ -601,7 +609,7 @@
 	fmt.Println("")
 }
 `
-	runner.Run(t, collision, func(t *testing.T, env *Env) {
+	WithOptions(InGOPATH()).Run(t, collision, func(t *testing.T, env *Env) {
 		env.OpenFile("x/main.go")
 		env.Await(
 			env.DiagnosticAtRegexp("x/main.go", "fmt.Println"),
@@ -618,12 +626,12 @@
 		badFile := fmt.Sprintf("%s/found packages main (main.go) and x (x.go) in %s/src/x", dir, env.Sandbox.GOPATH())
 		env.Await(
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1),
+				env.DoneWithChange(),
 				EmptyDiagnostics("x/main.go"),
 			),
 			NoDiagnostics(badFile),
 		)
-	}, InGOPATH())
+	})
 }
 
 const ardanLabsProxy = `
@@ -654,9 +662,9 @@
 	_ = conf.ErrHelpWanted
 }
 `
-	withOptions(
+	WithOptions(
 		ProxyFiles(ardanLabsProxy),
-	).run(t, ardanLabs, func(t *testing.T, env *Env) {
+	).Run(t, ardanLabs, func(t *testing.T, env *Env) {
 		// Expect a diagnostic with a suggested fix to add
 		// "github.com/ardanlabs/conf" to the go.mod file.
 		env.OpenFile("go.mod")
@@ -712,9 +720,9 @@
 go 1.12
 -- main.go --
 `
-	withOptions(
+	WithOptions(
 		ProxyFiles(ardanLabsProxy),
-	).run(t, emptyFile, func(t *testing.T, env *Env) {
+	).Run(t, emptyFile, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, `package main
 
@@ -757,22 +765,19 @@
 	fmt.Println("hi")
 }
 `
-	runner.Run(t, simplePackage, func(t *testing.T, env *Env) {
+	Run(t, simplePackage, func(t *testing.T, env *Env) {
 		env.OpenFile("a/a1.go")
 		env.CreateBuffer("a/a2.go", ``)
 		env.SaveBufferWithoutActions("a/a2.go")
 		env.Await(
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1),
+				env.DoneWithSave(),
 				NoDiagnostics("a/a1.go"),
 			),
 		)
 		env.EditBuffer("a/a2.go", fake.NewEdit(0, 0, 0, 0, `package a`))
 		env.Await(
-			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1),
-				NoDiagnostics("a/a1.go"),
-			),
+			OnceMet(env.DoneWithChange(), NoDiagnostics("a/a1.go")),
 		)
 	})
 }
@@ -805,7 +810,7 @@
 	Hello()
 }
 `
-	runner.Run(t, testVariant, func(t *testing.T, env *Env) {
+	Run(t, testVariant, func(t *testing.T, env *Env) {
 		// Open the file, triggering the workspace load.
 		// There are errors in the code to ensure all is working as expected.
 		env.OpenFile("hello/hello.go")
@@ -861,12 +866,10 @@
 package foo
 -- foo/bar_test.go --
 `
-	run(t, mod, func(t *testing.T, env *Env) {
+	Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("foo/bar_test.go")
 		env.EditBuffer("foo/bar_test.go", fake.NewEdit(0, 0, 0, 0, "package foo"))
-		env.Await(
-			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1),
-		)
+		env.Await(env.DoneWithChange())
 		env.RegexpReplace("foo/bar_test.go", "package foo", `package foo_test
 
 import "testing"
@@ -894,17 +897,17 @@
 -- foo/bar_test.go --
 package foo_
 `
-	run(t, mod, func(t *testing.T, env *Env) {
+	Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("foo/bar_test.go")
 		env.RegexpReplace("foo/bar_test.go", "package foo_", "package foo_test")
 		env.SaveBuffer("foo/bar_test.go")
 		env.Await(
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1),
+				env.DoneWithSave(),
 				NoDiagnostics("foo/bar_test.go"),
 			),
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1),
+				env.DoneWithSave(),
 				NoDiagnostics("foo/foo.go"),
 			),
 		)
@@ -919,11 +922,11 @@
 -- x_test.go --
 `
 
-	withOptions(InGOPATH()).run(t, files, func(t *testing.T, env *Env) {
+	WithOptions(InGOPATH()).Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("x_test.go")
 		env.EditBuffer("x_test.go", fake.NewEdit(0, 0, 0, 0, "pack"))
 		env.Await(
-			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1),
+			env.DoneWithChange(),
 			NoShowMessage(),
 		)
 	})
@@ -940,11 +943,11 @@
 
 var _ = foo.Bar
 `
-	runner.Run(t, ws, func(t *testing.T, env *Env) {
+	Run(t, ws, func(t *testing.T, env *Env) {
 		env.OpenFile("_foo/x.go")
 		env.Await(
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1),
+				env.DoneWithOpen(),
 				NoDiagnostics("_foo/x.go"),
 			))
 	})
@@ -978,9 +981,9 @@
 
 const C = a.A
 `
-	runner.Run(t, ws, func(t *testing.T, env *Env) {
+	Run(t, ws, func(t *testing.T, env *Env) {
 		env.OpenFile("b/b.go")
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1))
+		env.Await(env.DoneWithOpen())
 		// Delete c/c.go, the only file in package c.
 		env.RemoveWorkspaceFile("c/c.go")
 
@@ -1007,16 +1010,16 @@
 	// package loads.
 	writeGoVim := func(env *Env, name, content string) {
 		env.WriteWorkspaceFile(name, "")
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1))
+		env.Await(env.DoneWithChangeWatchedFiles())
 
 		env.CreateBuffer(name, "\n")
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1))
+		env.Await(env.DoneWithOpen())
 
 		env.EditBuffer(name, fake.NewEdit(1, 0, 1, 0, content))
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1))
+		env.Await(env.DoneWithChange())
 
 		env.EditBuffer(name, fake.NewEdit(0, 0, 1, 0, ""))
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1))
+		env.Await(env.DoneWithChange())
 	}
 
 	const p = `package p; func DoIt(s string) {};`
@@ -1031,7 +1034,7 @@
 	// A simple version of the test that reproduces most of the problems it
 	// exposes.
 	t.Run("short", func(t *testing.T) {
-		runner.Run(t, mod, func(t *testing.T, env *Env) {
+		Run(t, mod, func(t *testing.T, env *Env) {
 			writeGoVim(env, "p/p.go", p)
 			writeGoVim(env, "main.go", main)
 			env.Await(env.DiagnosticAtRegexp("main.go", "5"))
@@ -1040,7 +1043,7 @@
 
 	// A full version that replicates the whole flow of the test.
 	t.Run("full", func(t *testing.T) {
-		runner.Run(t, mod, func(t *testing.T, env *Env) {
+		Run(t, mod, func(t *testing.T, env *Env) {
 			writeGoVim(env, "p/p.go", p)
 			writeGoVim(env, "main.go", main)
 			writeGoVim(env, "p/p_test.go", `package p
@@ -1091,10 +1094,10 @@
 	var x int
 }
 `
-	withOptions(
+	WithOptions(
 		// Empty workspace folders.
 		WorkspaceFolders(),
-	).run(t, mod, func(t *testing.T, env *Env) {
+	).Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("a/a.go")
 		env.Await(
 			env.DiagnosticAtRegexp("a/a.go", "x"),
@@ -1119,7 +1122,7 @@
 	fmt.Println("")
 }
 `
-	runner.Run(t, basic, func(t *testing.T, env *Env) {
+	Run(t, basic, func(t *testing.T, env *Env) {
 		testenv.NeedsGo1Point(t, 15)
 
 		env.WriteWorkspaceFile("foo/foo_test.go", `package main
@@ -1131,7 +1134,7 @@
 		env.RegexpReplace("foo/foo_test.go", `package main`, `package foo`)
 		env.Await(
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1),
+				env.DoneWithChange(),
 				NoDiagnostics("foo/foo.go"),
 			),
 		)
@@ -1149,15 +1152,15 @@
 
 func main() {}
 `
-	runner.Run(t, basic, func(t *testing.T, env *Env) {
+	Run(t, basic, func(t *testing.T, env *Env) {
 		env.Editor.CreateBuffer(env.Ctx, "foo.go", `package main`)
 		env.Await(
-			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1),
+			env.DoneWithOpen(),
 		)
 		env.CloseBuffer("foo.go")
 		env.Await(
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidClose), 1),
+				env.DoneWithClose(),
 				NoLogMatching(protocol.Info, "packages=0"),
 			),
 		)
@@ -1174,31 +1177,25 @@
 -- main2.go --
 package main
 `
-	runner.Run(t, basic, func(t *testing.T, env *Env) {
+	Run(t, basic, func(t *testing.T, env *Env) {
 		env.CreateBuffer("main.go", "")
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1))
+		env.Await(env.DoneWithOpen())
 
 		env.SaveBufferWithoutActions("main.go")
-		env.Await(
-			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1),
-			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
-		)
+		env.Await(env.DoneWithSave(), env.DoneWithChangeWatchedFiles())
 
 		env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, `package main
 
 func main() {
 }
 `))
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1))
+		env.Await(env.DoneWithChange())
 
 		env.SaveBuffer("main.go")
-		env.Await(
-			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 2),
-			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2),
-		)
+		env.Await(env.DoneWithSave(), env.DoneWithChangeWatchedFiles())
 
 		env.EditBuffer("main.go", fake.NewEdit(0, 0, 4, 0, ""))
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 2))
+		env.Await(env.DoneWithChange())
 
 		env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, `package main
 
@@ -1226,7 +1223,7 @@
 
 func main() {}
 `
-	runner.Run(t, pkgDefault, func(t *testing.T, env *Env) {
+	Run(t, pkgDefault, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		env.Await(
 			env.DiagnosticAtRegexp("main.go", "default"),
@@ -1252,18 +1249,18 @@
 	var x int
 }
 `
-	withOptions(
+	WithOptions(
 		WorkspaceFolders("a"),
-	).run(t, mod, func(t *testing.T, env *Env) {
+	).Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("a/main.go")
 		env.Await(
 			env.DiagnosticAtRegexp("main.go", "x"),
 		)
 	})
-	withOptions(
+	WithOptions(
 		WorkspaceFolders("a"),
 		LimitWorkspaceScope(),
-	).run(t, mod, func(t *testing.T, env *Env) {
+	).Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("a/main.go")
 		env.Await(
 			NoDiagnostics("main.go"),
@@ -1292,9 +1289,9 @@
 }
 `
 
-	withOptions(
+	WithOptions(
 		EditorConfig{EnableStaticcheck: true},
-	).run(t, files, func(t *testing.T, env *Env) {
+	).Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		// Staticcheck should generate a diagnostic to simplify this literal.
 		env.Await(env.DiagnosticAtRegexp("main.go", `t{"msg"}`))
@@ -1317,7 +1314,7 @@
 package main
 func main() {}
 `
-	runner.Run(t, dir, func(t *testing.T, env *Env) {
+	Run(t, dir, func(t *testing.T, env *Env) {
 		log.SetFlags(log.Lshortfile)
 		env.OpenFile("main.go")
 		env.OpenFile("other.go")
@@ -1368,7 +1365,7 @@
 	var x int
 }
 `
-	run(t, files, func(t *testing.T, env *Env) {
+	Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("a/a.go")
 		env.Await(
 			env.DiagnosticAtRegexp("a/a.go", "x"),
@@ -1395,11 +1392,11 @@
 	_ = 1
 }
 `
-	withOptions(
+	WithOptions(
 		EditorConfig{
 			AllExperiments: true,
 		},
-	).run(t, mod, func(t *testing.T, env *Env) {
+	).Run(t, mod, func(t *testing.T, env *Env) {
 		// Confirm that the setting doesn't cause any warnings.
 		env.Await(NoShowMessage())
 	})
@@ -1442,7 +1439,7 @@
 	var x int
 }
 `
-	run(t, mod, func(t *testing.T, env *Env) {
+	Run(t, mod, func(t *testing.T, env *Env) {
 		env.Await(
 			OnceMet(
 				InitialWorkspaceLoad,
@@ -1459,6 +1456,25 @@
 func TestRenamePackage(t *testing.T) {
 	testenv.NeedsGo1Point(t, 16)
 
+	const proxy = `
+-- example.com@v1.2.3/go.mod --
+module example.com
+
+go 1.12
+-- example.com@v1.2.3/blah/blah.go --
+package blah
+
+const Name = "Blah"
+-- random.org@v1.2.3/go.mod --
+module random.org
+
+go 1.12
+-- random.org@v1.2.3/blah/blah.go --
+package hello
+
+const Name = "Hello"
+`
+
 	const contents = `
 -- go.mod --
 module mod.com
@@ -1480,17 +1496,17 @@
 package foo_
 `
 
-	withOptions(
+	WithOptions(
 		ProxyFiles(proxy),
 		InGOPATH(),
-	).run(t, contents, func(t *testing.T, env *Env) {
+	).Run(t, contents, func(t *testing.T, env *Env) {
 		// Simulate typing character by character.
 		env.OpenFile("foo/foo_test.go")
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1))
+		env.Await(env.DoneWithOpen())
 		env.RegexpReplace("foo/foo_test.go", "_", "_t")
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1))
+		env.Await(env.DoneWithChange())
 		env.RegexpReplace("foo/foo_test.go", "_t", "_test")
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 2))
+		env.Await(env.DoneWithChange())
 
 		env.Await(
 			EmptyDiagnostics("foo/foo_test.go"),
@@ -1512,7 +1528,7 @@
 -- main.go --
 package main
 `
-	run(t, pkg, func(t *testing.T, env *Env) {
+	Run(t, pkg, func(t *testing.T, env *Env) {
 		env.OpenFile("go.mod")
 		env.Await(
 			OutstandingWork(lsp.WorkspaceLoadFailure, "unknown directive"),
@@ -1556,7 +1572,7 @@
 	bob.Hello()
 }
 `
-	run(t, mod, func(t *testing.T, env *Env) {
+	Run(t, mod, func(t *testing.T, env *Env) {
 		env.RemoveWorkspaceFile("bob")
 		env.Await(
 			env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`),
@@ -1598,7 +1614,7 @@
 
 import _ "mod.com/triple/a"
 `
-	run(t, mod, func(t *testing.T, env *Env) {
+	Run(t, mod, func(t *testing.T, env *Env) {
 		env.Await(
 			env.DiagnosticAtRegexpWithMessage("self/self.go", `_ "mod.com/self"`, "import cycle not allowed"),
 			env.DiagnosticAtRegexpWithMessage("double/a/a.go", `_ "mod.com/double/b"`, "import cycle not allowed"),
@@ -1623,20 +1639,20 @@
 )
 `
 	t.Run("module", func(t *testing.T) {
-		run(t, mod, func(t *testing.T, env *Env) {
+		Run(t, mod, func(t *testing.T, env *Env) {
 			env.Await(
 				env.DiagnosticAtRegexpWithMessage("main.go", `"nosuchpkg"`, `could not import nosuchpkg (no required module provides package "nosuchpkg"`),
 			)
 		})
 	})
 	t.Run("GOPATH", func(t *testing.T) {
-		withOptions(
+		WithOptions(
 			InGOPATH(),
 			EditorConfig{
 				Env: map[string]string{"GO111MODULE": "off"},
 			},
 			Modes(Singleton),
-		).run(t, mod, func(t *testing.T, env *Env) {
+		).Run(t, mod, func(t *testing.T, env *Env) {
 			env.Await(
 				env.DiagnosticAtRegexpWithMessage("main.go", `"nosuchpkg"`, `cannot find package "nosuchpkg" in any of`),
 			)
@@ -1661,14 +1677,14 @@
 `
 	for _, go111module := range []string{"on", "auto"} {
 		t.Run("GO111MODULE="+go111module, func(t *testing.T) {
-			withOptions(
+			WithOptions(
 				Modes(Singleton),
 				EditorConfig{
 					Env: map[string]string{
 						"GO111MODULE": go111module,
 					},
 				},
-			).run(t, modules, func(t *testing.T, env *Env) {
+			).Run(t, modules, func(t *testing.T, env *Env) {
 				env.OpenFile("a/a.go")
 				env.OpenFile("b/go.mod")
 				env.Await(
@@ -1682,7 +1698,7 @@
 
 	// Expect no warning if GO111MODULE=auto in a directory in GOPATH.
 	t.Run("GOPATH_GO111MODULE_auto", func(t *testing.T) {
-		withOptions(
+		WithOptions(
 			Modes(Singleton),
 			EditorConfig{
 				Env: map[string]string{
@@ -1690,11 +1706,11 @@
 				},
 			},
 			InGOPATH(),
-		).run(t, modules, func(t *testing.T, env *Env) {
+		).Run(t, modules, func(t *testing.T, env *Env) {
 			env.OpenFile("a/a.go")
 			env.Await(
 				OnceMet(
-					CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1),
+					env.DoneWithOpen(),
 					NoDiagnostics("a/a.go"),
 				),
 				NoOutstandingWork(),
@@ -1747,13 +1763,13 @@
 
 func helloHelper() {}
 `
-	withOptions(
+	WithOptions(
 		ProxyFiles(proxy),
 		Modes(Singleton),
-	).run(t, nested, func(t *testing.T, env *Env) {
+	).Run(t, nested, func(t *testing.T, env *Env) {
 		// Expect a diagnostic in a nested module.
 		env.OpenFile("nested/hello/hello.go")
-		didOpen := CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1)
+		didOpen := env.DoneWithOpen()
 		env.Await(
 			OnceMet(
 				didOpen,
@@ -1778,12 +1794,12 @@
 
 func main() {}
 `
-	run(t, nomod, func(t *testing.T, env *Env) {
+	Run(t, nomod, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		env.RegexpReplace("main.go", "{}", "{ var x int; }") // simulate typing
 		env.Await(
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1),
+				env.DoneWithChange(),
 				NoLogMatching(protocol.Info, "packages=1"),
 			),
 		)
diff --git a/gopls/internal/regtest/env.go b/gopls/internal/regtest/env.go
index 70859fd..322799d 100644
--- a/gopls/internal/regtest/env.go
+++ b/gopls/internal/regtest/env.go
@@ -54,7 +54,7 @@
 	// be string, though the spec allows for numeric tokens as well.  When work
 	// completes, it is deleted from this map.
 	outstandingWork map[protocol.ProgressToken]*workProgress
-	completedWork   map[string]int
+	completedWork   map[string]uint64
 }
 
 type workProgress struct {
@@ -119,7 +119,7 @@
 		state: State{
 			diagnostics:     make(map[string]*protocol.PublishDiagnosticsParams),
 			outstandingWork: make(map[protocol.ProgressToken]*workProgress),
-			completedWork:   make(map[string]int),
+			completedWork:   make(map[string]uint64),
 		},
 		waiters: make(map[int]*condition),
 	}
diff --git a/gopls/internal/regtest/env_test.go b/gopls/internal/regtest/env_test.go
index 82fb17f..e476be9 100644
--- a/gopls/internal/regtest/env_test.go
+++ b/gopls/internal/regtest/env_test.go
@@ -16,7 +16,7 @@
 	e := &Env{
 		state: State{
 			outstandingWork: make(map[protocol.ProgressToken]*workProgress),
-			completedWork:   make(map[string]int),
+			completedWork:   make(map[string]uint64),
 		},
 	}
 	ctx := context.Background()
diff --git a/gopls/internal/regtest/expectation.go b/gopls/internal/regtest/expectation.go
index 6c479c3..037faa4 100644
--- a/gopls/internal/regtest/expectation.go
+++ b/gopls/internal/regtest/expectation.go
@@ -199,6 +199,13 @@
 	return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), changes)
 }
 
+// DoneWithSave expects all didSave notifications currently sent by the editor
+// to be completely processed.
+func (e *Env) DoneWithSave() Expectation {
+	saves := e.Editor.Stats().DidSave
+	return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), saves)
+}
+
 // DoneWithChangeWatchedFiles expects all didChangeWatchedFiles notifications
 // currently sent by the editor to be completely processed.
 func (e *Env) DoneWithChangeWatchedFiles() Expectation {
@@ -206,11 +213,18 @@
 	return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), changes)
 }
 
+// DoneWithClose expects all didClose notifications currently sent by the
+// editor to be completely processed.
+func (e *Env) DoneWithClose() Expectation {
+	changes := e.Editor.Stats().DidClose
+	return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidClose), changes)
+}
+
 // CompletedWork expects a work item to have been completed >= atLeast times.
 //
 // Since the Progress API doesn't include any hidden metadata, we must use the
 // progress notification title to identify the work we expect to be completed.
-func CompletedWork(title string, atLeast int) SimpleExpectation {
+func CompletedWork(title string, atLeast uint64) SimpleExpectation {
 	check := func(s State) Verdict {
 		if s.completedWork[title] >= atLeast {
 			return Met
diff --git a/gopls/internal/regtest/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go
similarity index 79%
rename from gopls/internal/regtest/configuration_test.go
rename to gopls/internal/regtest/misc/configuration_test.go
index b61a8a8..d299e3f 100644
--- a/gopls/internal/regtest/configuration_test.go
+++ b/gopls/internal/regtest/misc/configuration_test.go
@@ -2,12 +2,13 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package misc
 
 import (
 	"testing"
 
-	"golang.org/x/tools/internal/lsp"
+	. "golang.org/x/tools/gopls/internal/regtest"
+
 	"golang.org/x/tools/internal/lsp/fake"
 )
 
@@ -25,16 +26,16 @@
 // NotThisVariable should really start with ThisVariable.
 const ThisVariable = 7
 `
-	run(t, files, func(t *testing.T, env *Env) {
+	Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("a/a.go")
 		env.Await(
-			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1),
+			env.DoneWithOpen(),
 			NoDiagnostics("a/a.go"),
 		)
 		cfg := &fake.EditorConfig{}
 		*cfg = env.Editor.Config
 		cfg.EnableStaticcheck = true
-		env.changeConfiguration(t, cfg)
+		env.ChangeConfiguration(t, cfg)
 		env.Await(
 			DiagnosticAt("a/a.go", 2, 0),
 		)
diff --git a/gopls/internal/regtest/definition_test.go b/gopls/internal/regtest/misc/definition_test.go
similarity index 91%
rename from gopls/internal/regtest/definition_test.go
rename to gopls/internal/regtest/misc/definition_test.go
index 36a9352..a5e220c 100644
--- a/gopls/internal/regtest/definition_test.go
+++ b/gopls/internal/regtest/misc/definition_test.go
@@ -2,13 +2,15 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package misc
 
 import (
 	"path"
 	"strings"
 	"testing"
 
+	. "golang.org/x/tools/gopls/internal/regtest"
+
 	"golang.org/x/tools/internal/lsp/tests"
 )
 
@@ -32,7 +34,7 @@
 `
 
 func TestGoToInternalDefinition(t *testing.T) {
-	runner.Run(t, internalDefinition, func(t *testing.T, env *Env) {
+	Run(t, internalDefinition, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		name, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", "message"))
 		if want := "const.go"; name != want {
@@ -59,7 +61,7 @@
 }`
 
 func TestGoToStdlibDefinition_Issue37045(t *testing.T) {
-	runner.Run(t, stdlibDefinition, func(t *testing.T, env *Env) {
+	Run(t, stdlibDefinition, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		name, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `fmt.(Printf)`))
 		if got, want := path.Base(name), "print.go"; got != want {
@@ -79,7 +81,7 @@
 }
 
 func TestUnexportedStdlib_Issue40809(t *testing.T) {
-	runner.Run(t, stdlibDefinition, func(t *testing.T, env *Env) {
+	Run(t, stdlibDefinition, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		name, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `fmt.(Printf)`))
 		env.OpenFile(name)
@@ -120,7 +122,7 @@
 	var err error
 	err.Error()
 }`
-	run(t, mod, func(t *testing.T, env *Env) {
+	Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		content, _ := env.Hover("main.go", env.RegexpSearch("main.go", "Error"))
 		if content == nil {
diff --git a/gopls/internal/regtest/failures_test.go b/gopls/internal/regtest/misc/failures_test.go
similarity index 89%
rename from gopls/internal/regtest/failures_test.go
rename to gopls/internal/regtest/misc/failures_test.go
index 4da6ce8..68bbded 100644
--- a/gopls/internal/regtest/failures_test.go
+++ b/gopls/internal/regtest/misc/failures_test.go
@@ -2,11 +2,13 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package misc
 
 import (
 	"log"
 	"testing"
+
+	. "golang.org/x/tools/gopls/internal/regtest"
 )
 
 // This test passes (TestHoverOnError in definition_test.go) without
@@ -28,7 +30,7 @@
 	var err error
 	err.Error()
 }`
-	withOptions(SkipLogs()).run(t, mod, func(t *testing.T, env *Env) {
+	WithOptions(SkipLogs()).Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		content, _ := env.Hover("main.go", env.RegexpSearch("main.go", "Error"))
 		// without the //line comment content would be non-nil
@@ -56,7 +58,7 @@
 `
 
 func TestFailingDiagnosticClearingOnEdit(t *testing.T) {
-	runner.Run(t, badPackageDup, func(t *testing.T, env *Env) {
+	Run(t, badPackageDup, func(t *testing.T, env *Env) {
 		log.SetFlags(log.Lshortfile)
 		env.OpenFile("b.go")
 		env.Await(env.AnyDiagnosticAtCurrentVersion("a.go"))
diff --git a/gopls/internal/regtest/fix_test.go b/gopls/internal/regtest/misc/fix_test.go
similarity index 92%
rename from gopls/internal/regtest/fix_test.go
rename to gopls/internal/regtest/misc/fix_test.go
index e513148..a256609 100644
--- a/gopls/internal/regtest/fix_test.go
+++ b/gopls/internal/regtest/misc/fix_test.go
@@ -2,11 +2,13 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package misc
 
 import (
 	"testing"
 
+	. "golang.org/x/tools/gopls/internal/regtest"
+
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/tests"
 )
@@ -27,7 +29,7 @@
 	_ = types.Info{}
 }
 `
-	runner.Run(t, basic, func(t *testing.T, env *Env) {
+	Run(t, basic, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		if err := env.Editor.RefactorRewrite(env.Ctx, "main.go", &protocol.Range{
 			Start: protocol.Position{
diff --git a/gopls/internal/regtest/formatting_test.go b/gopls/internal/regtest/misc/formatting_test.go
similarity index 88%
rename from gopls/internal/regtest/formatting_test.go
rename to gopls/internal/regtest/misc/formatting_test.go
index 63fa0ce1..0ad5fbb 100644
--- a/gopls/internal/regtest/formatting_test.go
+++ b/gopls/internal/regtest/misc/formatting_test.go
@@ -2,13 +2,14 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package misc
 
 import (
 	"strings"
 	"testing"
 
-	"golang.org/x/tools/internal/lsp"
+	. "golang.org/x/tools/gopls/internal/regtest"
+
 	"golang.org/x/tools/internal/lsp/tests"
 )
 
@@ -30,7 +31,7 @@
 `
 
 func TestFormatting(t *testing.T) {
-	runner.Run(t, unformattedProgram, func(t *testing.T, env *Env) {
+	Run(t, unformattedProgram, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		env.FormatBuffer("main.go")
 		got := env.Editor.BufferText("main.go")
@@ -52,7 +53,7 @@
 
 func f() {}
 `
-	runner.Run(t, onelineProgram, func(t *testing.T, env *Env) {
+	Run(t, onelineProgram, func(t *testing.T, env *Env) {
 		env.OpenFile("a.go")
 		env.FormatBuffer("a.go")
 		got := env.Editor.BufferText("a.go")
@@ -76,7 +77,7 @@
 
 func f() { fmt.Println() }
 `
-	runner.Run(t, onelineProgramA, func(t *testing.T, env *Env) {
+	Run(t, onelineProgramA, func(t *testing.T, env *Env) {
 		env.OpenFile("a.go")
 		env.OrganizeImports("a.go")
 		got := env.Editor.BufferText("a.go")
@@ -97,7 +98,7 @@
 
 func f() {}
 `
-	runner.Run(t, onelineProgramB, func(t *testing.T, env *Env) {
+	Run(t, onelineProgramB, func(t *testing.T, env *Env) {
 		env.OpenFile("a.go")
 		env.OrganizeImports("a.go")
 		got := env.Editor.BufferText("a.go")
@@ -143,7 +144,7 @@
 `
 
 func TestOrganizeImports(t *testing.T) {
-	runner.Run(t, disorganizedProgram, func(t *testing.T, env *Env) {
+	Run(t, disorganizedProgram, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		env.OrganizeImports("main.go")
 		got := env.Editor.BufferText("main.go")
@@ -155,7 +156,7 @@
 }
 
 func TestFormattingOnSave(t *testing.T) {
-	runner.Run(t, disorganizedProgram, func(t *testing.T, env *Env) {
+	Run(t, disorganizedProgram, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		env.SaveBuffer("main.go")
 		got := env.Editor.BufferText("main.go")
@@ -225,10 +226,10 @@
 		},
 	} {
 		t.Run(tt.issue, func(t *testing.T) {
-			run(t, "-- main.go --", func(t *testing.T, env *Env) {
+			Run(t, "-- main.go --", func(t *testing.T, env *Env) {
 				crlf := strings.ReplaceAll(tt.want, "\n", "\r\n")
 				env.CreateBuffer("main.go", crlf)
-				env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1))
+				env.Await(env.DoneWithOpen())
 				env.OrganizeImports("main.go")
 				got := env.Editor.BufferText("main.go")
 				got = strings.ReplaceAll(got, "\r\n", "\n") // convert everything to LF for simplicity
diff --git a/gopls/internal/regtest/generate_test.go b/gopls/internal/regtest/misc/generate_test.go
similarity index 81%
rename from gopls/internal/regtest/generate_test.go
rename to gopls/internal/regtest/misc/generate_test.go
index 87e64df..6987924 100644
--- a/gopls/internal/regtest/generate_test.go
+++ b/gopls/internal/regtest/misc/generate_test.go
@@ -6,12 +6,12 @@
 
 // +build !android
 
-package regtest
+package misc
 
 import (
 	"testing"
 
-	"golang.org/x/tools/internal/lsp"
+	. "golang.org/x/tools/gopls/internal/regtest"
 )
 
 func TestGenerateProgress(t *testing.T) {
@@ -40,14 +40,14 @@
 //go:generate go run generate.go
 `
 
-	runner.Run(t, generatedWorkspace, func(t *testing.T, env *Env) {
+	Run(t, generatedWorkspace, func(t *testing.T, env *Env) {
 		env.Await(
 			env.DiagnosticAtRegexp("lib/lib.go", "answer"),
 		)
 		env.RunGenerate("./lib")
 		env.Await(
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+				env.DoneWithChangeWatchedFiles(),
 				EmptyDiagnostics("lib/lib.go")),
 		)
 	})
diff --git a/gopls/internal/regtest/imports_test.go b/gopls/internal/regtest/misc/imports_test.go
similarity index 92%
rename from gopls/internal/regtest/imports_test.go
rename to gopls/internal/regtest/misc/imports_test.go
index 1c52d59..9a95208 100644
--- a/gopls/internal/regtest/imports_test.go
+++ b/gopls/internal/regtest/misc/imports_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package misc
 
 import (
 	"io/ioutil"
@@ -11,6 +11,8 @@
 	"strings"
 	"testing"
 
+	. "golang.org/x/tools/gopls/internal/regtest"
+
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/testenv"
 )
@@ -42,7 +44,7 @@
 
 	// it was returning
 	// "package main\nimport \"testing\"\npackage main..."
-	runner.Run(t, needs, func(t *testing.T, env *Env) {
+	Run(t, needs, func(t *testing.T, env *Env) {
 		env.CreateBuffer("a_test.go", ntest)
 		env.SaveBuffer("a_test.go")
 		got := env.Editor.BufferText("a_test.go")
@@ -69,7 +71,7 @@
 
 	// The file remains unchanged, but if there are any CodeActions returned, they confuse vim.
 	// Therefore check for no CodeActions
-	runner.Run(t, "", func(t *testing.T, env *Env) {
+	Run(t, "", func(t *testing.T, env *Env) {
 		env.CreateBuffer("main.go", vim1)
 		env.OrganizeImports("main.go")
 		actions := env.CodeAction("main.go")
@@ -102,7 +104,7 @@
 }
 `
 
-	runner.Run(t, "", func(t *testing.T, env *Env) {
+	Run(t, "", func(t *testing.T, env *Env) {
 		env.CreateBuffer("main.go", vim2)
 		env.OrganizeImports("main.go")
 		actions := env.CodeAction("main.go")
@@ -152,10 +154,10 @@
 	}
 	defer os.RemoveAll(modcache)
 	editorConfig := EditorConfig{Env: map[string]string{"GOMODCACHE": modcache}}
-	withOptions(
+	WithOptions(
 		editorConfig,
 		ProxyFiles(proxy),
-	).run(t, files, func(t *testing.T, env *Env) {
+	).Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		env.Await(env.DiagnosticAtRegexp("main.go", `y.Y`))
 		env.SaveBuffer("main.go")
@@ -197,7 +199,7 @@
 	os.Stat("")
 }
 `
-	run(t, pkg, func(t *testing.T, env *Env) {
+	Run(t, pkg, func(t *testing.T, env *Env) {
 		env.OpenFile("a/a.go")
 		var d protocol.PublishDiagnosticsParams
 		env.Await(
diff --git a/gopls/internal/regtest/link_test.go b/gopls/internal/regtest/misc/link_test.go
similarity index 94%
rename from gopls/internal/regtest/link_test.go
rename to gopls/internal/regtest/misc/link_test.go
index 1e662a4..320a3ea 100644
--- a/gopls/internal/regtest/link_test.go
+++ b/gopls/internal/regtest/misc/link_test.go
@@ -2,12 +2,14 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package misc
 
 import (
 	"strings"
 	"testing"
 
+	. "golang.org/x/tools/gopls/internal/regtest"
+
 	"golang.org/x/tools/internal/testenv"
 )
 
@@ -42,7 +44,9 @@
 
 const Hello = "Hello"
 `
-	runner.Run(t, program, func(t *testing.T, env *Env) {
+	WithOptions(
+		ProxyFiles(proxy),
+	).Run(t, program, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		env.OpenFile("go.mod")
 
@@ -87,5 +91,5 @@
 		if len(links) != 0 {
 			t.Errorf("documentLink: got %d document links for go.mod, want 0\nlinks: %v", len(links), links)
 		}
-	}, ProxyFiles(proxy))
+	})
 }
diff --git a/gopls/internal/regtest/misc/misc_test.go b/gopls/internal/regtest/misc/misc_test.go
new file mode 100644
index 0000000..0f42470
--- /dev/null
+++ b/gopls/internal/regtest/misc/misc_test.go
@@ -0,0 +1,15 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package misc
+
+import (
+	"testing"
+
+	"golang.org/x/tools/gopls/internal/regtest"
+)
+
+func TestMain(m *testing.M) {
+	regtest.Main(m)
+}
diff --git a/gopls/internal/regtest/references_test.go b/gopls/internal/regtest/misc/references_test.go
similarity index 88%
rename from gopls/internal/regtest/references_test.go
rename to gopls/internal/regtest/misc/references_test.go
index db94ed8..9327636 100644
--- a/gopls/internal/regtest/references_test.go
+++ b/gopls/internal/regtest/misc/references_test.go
@@ -2,10 +2,12 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package misc
 
 import (
 	"testing"
+
+	. "golang.org/x/tools/gopls/internal/regtest"
 )
 
 func TestStdlibReferences(t *testing.T) {
@@ -24,7 +26,7 @@
 }
 `
 
-	run(t, files, func(t *testing.T, env *Env) {
+	Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		file, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `fmt.(Print)`))
 		refs, err := env.Editor.References(env.Ctx, file, pos)
diff --git a/gopls/internal/regtest/shared_test.go b/gopls/internal/regtest/misc/shared_test.go
similarity index 78%
rename from gopls/internal/regtest/shared_test.go
rename to gopls/internal/regtest/misc/shared_test.go
index 10c39b0..376d378 100644
--- a/gopls/internal/regtest/shared_test.go
+++ b/gopls/internal/regtest/misc/shared_test.go
@@ -2,10 +2,12 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package misc
 
 import (
 	"testing"
+
+	. "golang.org/x/tools/gopls/internal/regtest"
 )
 
 const sharedProgram = `
@@ -22,19 +24,19 @@
 	fmt.Println("Hello World.")
 }`
 
-func runShared(t *testing.T, program string, testFunc func(env1 *Env, env2 *Env)) {
+func runShared(t *testing.T, testFunc func(env1 *Env, env2 *Env)) {
 	// Only run these tests in forwarded modes.
-	modes := runner.DefaultModes & (Forwarded | SeparateProcess)
-	runner.Run(t, sharedProgram, func(t *testing.T, env1 *Env) {
+	modes := DefaultModes() & (Forwarded | SeparateProcess)
+	WithOptions(Modes(modes)).Run(t, sharedProgram, func(t *testing.T, env1 *Env) {
 		// Create a second test session connected to the same workspace and server
 		// as the first.
 		env2 := NewEnv(env1.Ctx, t, env1.Sandbox, env1.Server, env1.Editor.Config, true)
 		testFunc(env1, env2)
-	}, Modes(modes))
+	})
 }
 
 func TestSimultaneousEdits(t *testing.T) {
-	runShared(t, exampleProgram, func(env1 *Env, env2 *Env) {
+	runShared(t, func(env1 *Env, env2 *Env) {
 		// In editor #1, break fmt.Println as before.
 		env1.OpenFile("main.go")
 		env1.RegexpReplace("main.go", "Printl(n)", "")
@@ -49,7 +51,7 @@
 }
 
 func TestShutdown(t *testing.T) {
-	runShared(t, sharedProgram, func(env1 *Env, env2 *Env) {
+	runShared(t, func(env1 *Env, env2 *Env) {
 		env1.CloseEditor()
 		// Now make an edit in editor #2 to trigger diagnostics.
 		env2.OpenFile("main.go")
diff --git a/gopls/internal/regtest/vendor_test.go b/gopls/internal/regtest/misc/vendor_test.go
similarity index 90%
rename from gopls/internal/regtest/vendor_test.go
rename to gopls/internal/regtest/misc/vendor_test.go
index f9d43ee..0263090 100644
--- a/gopls/internal/regtest/vendor_test.go
+++ b/gopls/internal/regtest/misc/vendor_test.go
@@ -2,12 +2,13 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package misc
 
 import (
 	"testing"
 
-	"golang.org/x/tools/internal/lsp"
+	. "golang.org/x/tools/gopls/internal/regtest"
+
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/testenv"
@@ -49,10 +50,10 @@
 }
 `
 	// TODO(rstambler): Remove this when golang/go#41819 is resolved.
-	withOptions(
+	WithOptions(
 		Modes(Singleton),
 		ProxyFiles(basicProxy),
-	).run(t, pkgThatUsesVendoring, func(t *testing.T, env *Env) {
+	).Run(t, pkgThatUsesVendoring, func(t *testing.T, env *Env) {
 		env.OpenFile("a/a1.go")
 		env.Await(
 			// The editor should pop up a message suggesting that the user
@@ -61,7 +62,7 @@
 			// so once we see the request, we can assume that `go mod vendor`
 			// will be executed.
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1),
+				env.DoneWithOpen(),
 				env.DiagnosticAtRegexp("go.mod", "module mod.com"),
 			),
 		)
diff --git a/gopls/internal/regtest/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go
similarity index 85%
rename from gopls/internal/regtest/modfile_test.go
rename to gopls/internal/regtest/modfile/modfile_test.go
index faef053..86154db 100644
--- a/gopls/internal/regtest/modfile_test.go
+++ b/gopls/internal/regtest/modfile/modfile_test.go
@@ -2,19 +2,47 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package modfile
 
 import (
 	"path/filepath"
 	"strings"
 	"testing"
 
-	"golang.org/x/tools/internal/lsp"
+	. "golang.org/x/tools/gopls/internal/regtest"
+
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/tests"
 	"golang.org/x/tools/internal/testenv"
 )
 
+func TestMain(m *testing.M) {
+	Main(m)
+}
+
+const workspaceProxy = `
+-- example.com@v1.2.3/go.mod --
+module example.com
+
+go 1.12
+-- example.com@v1.2.3/blah/blah.go --
+package blah
+
+func SaySomething() {
+	fmt.Println("something")
+}
+-- random.org@v1.2.3/go.mod --
+module random.org
+
+go 1.12
+-- random.org@v1.2.3/bye/bye.go --
+package bye
+
+func Goodbye() {
+	println("Bye")
+}
+`
+
 const proxy = `
 -- example.com@v1.2.3/go.mod --
 module example.com
@@ -51,13 +79,13 @@
 }
 `
 
-	runner := runMultiple{
-		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
-		{"nested", withOptions(ProxyFiles(proxy))},
+	runner := RunMultiple{
+		{"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
+		{"nested", WithOptions(ProxyFiles(proxy))},
 	}
 
 	t.Run("basic", func(t *testing.T) {
-		runner.run(t, untidyModule, func(t *testing.T, env *Env) {
+		runner.Run(t, untidyModule, func(t *testing.T, env *Env) {
 			// Open the file and make sure that the initial workspace load does not
 			// modify the go.mod file.
 			goModContent := env.ReadWorkspaceFile("a/go.mod")
@@ -84,7 +112,7 @@
 	t.Run("delete main.go", func(t *testing.T) {
 		t.Skip("This test will be flaky until golang/go#40269 is resolved.")
 
-		runner.run(t, untidyModule, func(t *testing.T, env *Env) {
+		runner.Run(t, untidyModule, func(t *testing.T, env *Env) {
 			goModContent := env.ReadWorkspaceFile("a/go.mod")
 			mainContent := env.ReadWorkspaceFile("a/main.go")
 			env.OpenFile("a/main.go")
@@ -92,9 +120,9 @@
 
 			env.RemoveWorkspaceFile("a/main.go")
 			env.Await(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1),
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1),
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2),
+				env.DoneWithOpen(),
+				env.DoneWithSave(),
+				env.DoneWithChangeWatchedFiles(),
 			)
 
 			env.WriteWorkspaceFile("main.go", mainContent)
@@ -131,10 +159,10 @@
 require example.com v1.2.3
 `
 
-	runMultiple{
-		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
-		{"nested", withOptions(ProxyFiles(proxy))},
-	}.run(t, mod, func(t *testing.T, env *Env) {
+	RunMultiple{
+		{"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
+		{"nested", WithOptions(ProxyFiles(proxy))},
+	}.Run(t, mod, func(t *testing.T, env *Env) {
 		if strings.Contains(t.Name(), "workspace_module") {
 			t.Skip("workspace module mode doesn't set -mod=readonly")
 		}
@@ -184,10 +212,10 @@
 require random.org v1.2.3
 `
 
-	runMultiple{
-		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
-		{"nested", withOptions(ProxyFiles(proxy))},
-	}.run(t, mod, func(t *testing.T, env *Env) {
+	RunMultiple{
+		{"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
+		{"nested", WithOptions(ProxyFiles(proxy))},
+	}.Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("a/main.go")
 		var d protocol.PublishDiagnosticsParams
 		env.Await(
@@ -237,10 +265,10 @@
 require example.com v1.2.3
 `
 
-	runMultiple{
-		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
-		{"nested", withOptions(ProxyFiles(proxy))},
-	}.run(t, mod, func(t *testing.T, env *Env) {
+	RunMultiple{
+		{"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
+		{"nested", WithOptions(ProxyFiles(proxy))},
+	}.Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("a/go.mod")
 		var d protocol.PublishDiagnosticsParams
 		env.Await(
@@ -282,10 +310,10 @@
 go 1.14
 `
 
-	runMultiple{
-		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
-		{"nested", withOptions(ProxyFiles(proxy))},
-	}.run(t, files, func(t *testing.T, env *Env) {
+	RunMultiple{
+		{"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
+		{"nested", WithOptions(ProxyFiles(proxy))},
+	}.Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("a/go.mod")
 		var d protocol.PublishDiagnosticsParams
 		env.Await(
@@ -345,10 +373,10 @@
     caire.RemoveTempImage()
 }`
 
-	runMultiple{
-		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
-		{"nested", withOptions(ProxyFiles(proxy))},
-	}.run(t, repro, func(t *testing.T, env *Env) {
+	RunMultiple{
+		{"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
+		{"nested", WithOptions(ProxyFiles(proxy))},
+	}.Run(t, repro, func(t *testing.T, env *Env) {
 		env.OpenFile("a/main.go")
 		var d protocol.PublishDiagnosticsParams
 		env.Await(
@@ -395,10 +423,10 @@
 func main() {
 	fmt.Println(blah.Name)
 `
-	runMultiple{
-		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
-		{"nested", withOptions(ProxyFiles(proxy))},
-	}.run(t, mod, func(t *testing.T, env *Env) {
+	RunMultiple{
+		{"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
+		{"nested", WithOptions(ProxyFiles(proxy))},
+	}.Run(t, mod, func(t *testing.T, env *Env) {
 		env.Await(env.DiagnosticAtRegexp("a/go.mod", "require"))
 		env.RunGoCommandInDir("a", "mod", "tidy")
 		env.Await(
@@ -452,10 +480,10 @@
 
 var _ = blah.Name
 `
-	runMultiple{
-		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
-		{"nested", withOptions(ProxyFiles(proxy))},
-	}.run(t, files, func(t *testing.T, env *Env) {
+	RunMultiple{
+		{"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
+		{"nested", WithOptions(ProxyFiles(proxy))},
+	}.Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("a/main.go")
 		env.OpenFile("a/go.mod")
 		var d protocol.PublishDiagnosticsParams
@@ -503,13 +531,13 @@
 }
 `
 
-	runner := runMultiple{
-		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
-		{"nested", withOptions(ProxyFiles(proxy))},
+	runner := RunMultiple{
+		{"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
+		{"nested", WithOptions(ProxyFiles(proxy))},
 	}
 	// Start from a bad state/bad IWL, and confirm that we recover.
 	t.Run("bad", func(t *testing.T) {
-		runner.run(t, unknown, func(t *testing.T, env *Env) {
+		runner.Run(t, unknown, func(t *testing.T, env *Env) {
 			env.OpenFile("a/go.mod")
 			env.Await(
 				env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.2"),
@@ -555,7 +583,7 @@
 	// Start from a good state, transform to a bad state, and confirm that we
 	// still recover.
 	t.Run("good", func(t *testing.T) {
-		runner.run(t, known, func(t *testing.T, env *Env) {
+		runner.Run(t, known, func(t *testing.T, env *Env) {
 			env.OpenFile("a/go.mod")
 			env.Await(
 				env.DiagnosticAtRegexp("a/main.go", "x = "),
@@ -615,10 +643,10 @@
 	println(blah.Name)
 }
 `
-	runMultiple{
-		{"default", withOptions(ProxyFiles(badProxy), WorkspaceFolders("a"))},
-		{"nested", withOptions(ProxyFiles(badProxy))},
-	}.run(t, module, func(t *testing.T, env *Env) {
+	RunMultiple{
+		{"default", WithOptions(ProxyFiles(badProxy), WorkspaceFolders("a"))},
+		{"nested", WithOptions(ProxyFiles(badProxy))},
+	}.Run(t, module, func(t *testing.T, env *Env) {
 		env.OpenFile("a/go.mod")
 		env.Await(
 			env.DiagnosticAtRegexp("a/go.mod", "require example.com v1.2.3"),
@@ -642,7 +670,7 @@
 	println(blah.Name)
 }
 `
-	withOptions(
+	WithOptions(
 		EditorConfig{
 			Env: map[string]string{
 				"GOFLAGS": "-mod=readonly",
@@ -650,7 +678,7 @@
 		},
 		ProxyFiles(proxy),
 		Modes(Singleton),
-	).run(t, mod, func(t *testing.T, env *Env) {
+	).Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		original := env.ReadWorkspaceFile("go.mod")
 		env.Await(
@@ -673,19 +701,22 @@
 
 	const mod = `
 -- a/go.mod --
-module mod.com
+module moda.com
 
 go 1.14
 
 require (
 	example.com v1.2.3
 )
+-- a/go.sum --
+example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c=
+example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo=
 -- a/main.go --
 package main
 
 func main() {}
 -- b/go.mod --
-module mod.com
+module modb.com
 
 go 1.14
 -- b/main.go --
@@ -697,13 +728,13 @@
 	blah.SaySomething()
 }
 `
-	withOptions(
+	WithOptions(
 		ProxyFiles(workspaceProxy),
 		Modes(Experimental),
-	).run(t, mod, func(t *testing.T, env *Env) {
+	).Run(t, mod, func(t *testing.T, env *Env) {
 		env.Await(
-			env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.3"),
-			env.DiagnosticAtRegexp("b/go.mod", "module mod.com"),
+			env.DiagnosticAtRegexpWithMessage("a/go.mod", "example.com v1.2.3", "is not used"),
+			env.DiagnosticAtRegexpWithMessage("b/go.mod", "module modb.com", "not in your go.mod file"),
 		)
 	})
 }
@@ -727,12 +758,12 @@
 	blah.SaySomething()
 }
 `
-	withOptions(
+	WithOptions(
 		ProxyFiles(workspaceProxy),
 		EditorConfig{
 			BuildFlags: []string{"-tags", "bob"},
 		},
-	).run(t, mod, func(t *testing.T, env *Env) {
+	).Run(t, mod, func(t *testing.T, env *Env) {
 		env.Await(
 			env.DiagnosticAtRegexp("main.go", `"example.com/blah"`),
 		)
@@ -750,7 +781,7 @@
 
 func main() {}
 `
-	run(t, mod, func(t *testing.T, env *Env) {
+	Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("go.mod")
 		env.RegexpReplace("go.mod", "module", "modul")
 		env.Await(
@@ -783,10 +814,10 @@
 	println(blah.Name)
 }
 `
-	withOptions(
+	WithOptions(
 		Modes(Singleton), // workspace modules don't use -mod=readonly (golang/go#43346)
 		ProxyFiles(workspaceProxy),
-	).run(t, mod, func(t *testing.T, env *Env) {
+	).Run(t, mod, func(t *testing.T, env *Env) {
 		d := &protocol.PublishDiagnosticsParams{}
 		env.OpenFile("go.mod")
 		env.Await(
@@ -821,14 +852,14 @@
 
 func hello() {}
 `
-	withOptions(
+	WithOptions(
 		// TODO(rFindley) this doesn't work in multi-module workspace mode, because
 		// it keeps around the last parsing modfile. Update this test to also
 		// exercise the workspace module.
 		Modes(Singleton),
-	).run(t, mod, func(t *testing.T, env *Env) {
+	).Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("go.mod")
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1))
+		env.Await(env.DoneWithOpen())
 		env.RegexpReplace("go.mod", "module", "modul")
 		// Confirm that we still have metadata with only on-disk edits.
 		env.OpenFile("main.go")
@@ -892,9 +923,9 @@
 
 func main() {}
 `
-		withOptions(
+		WithOptions(
 			ProxyFiles(proxy),
-		).run(t, mod, func(t *testing.T, env *Env) {
+		).Run(t, mod, func(t *testing.T, env *Env) {
 			d := &protocol.PublishDiagnosticsParams{}
 			env.Await(
 				OnceMet(
@@ -934,9 +965,9 @@
 
 func main() {}
 `
-		withOptions(
+		WithOptions(
 			ProxyFiles(proxy),
-		).run(t, mod, func(t *testing.T, env *Env) {
+		).Run(t, mod, func(t *testing.T, env *Env) {
 			d := &protocol.PublishDiagnosticsParams{}
 			env.OpenFile("go.mod")
 			pos := env.RegexpSearch("go.mod", "require hasdep.com v1.2.3")
@@ -992,10 +1023,10 @@
 	blah.Hello()
 }
 `
-	withOptions(
+	WithOptions(
 		ProxyFiles(workspaceProxy),
 		Modes(Singleton),
-	).run(t, mod, func(t *testing.T, env *Env) {
+	).Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("go.mod")
 		pos := env.RegexpSearch("go.mod", "example.com")
 		params := &protocol.PublishDiagnosticsParams{}
diff --git a/gopls/internal/regtest/reg_test.go b/gopls/internal/regtest/regtest.go
similarity index 75%
rename from gopls/internal/regtest/reg_test.go
rename to gopls/internal/regtest/regtest.go
index 25accb1..87ff3f6 100644
--- a/gopls/internal/regtest/reg_test.go
+++ b/gopls/internal/regtest/regtest.go
@@ -30,14 +30,14 @@
 var runner *Runner
 
 type regtestRunner interface {
-	run(t *testing.T, files string, f TestFunc)
+	Run(t *testing.T, files string, f TestFunc)
 }
 
-func run(t *testing.T, files string, f TestFunc) {
+func Run(t *testing.T, files string, f TestFunc) {
 	runner.Run(t, files, f)
 }
 
-func withOptions(opts ...RunOption) configuredRunner {
+func WithOptions(opts ...RunOption) configuredRunner {
 	return configuredRunner{opts: opts}
 }
 
@@ -45,23 +45,35 @@
 	opts []RunOption
 }
 
-func (r configuredRunner) run(t *testing.T, files string, f TestFunc) {
+func (r configuredRunner) Run(t *testing.T, files string, f TestFunc) {
 	runner.Run(t, files, f, r.opts...)
 }
 
-type runMultiple []struct {
-	name   string
-	runner regtestRunner
+type RunMultiple []struct {
+	Name   string
+	Runner regtestRunner
 }
 
-func (r runMultiple) run(t *testing.T, files string, f TestFunc) {
+func (r RunMultiple) Run(t *testing.T, files string, f TestFunc) {
 	for _, runner := range r {
-		t.Run(runner.name, func(t *testing.T) {
-			runner.runner.run(t, files, f)
+		t.Run(runner.Name, func(t *testing.T) {
+			runner.Runner.Run(t, files, f)
 		})
 	}
 }
-func TestMain(m *testing.M) {
+
+func DefaultModes() Mode {
+	if *runSubprocessTests {
+		return NormalModes | SeparateProcess
+	}
+	return NormalModes
+}
+
+// Main sets up and tears down the shared regtest state.
+//
+// TODO(rFindley): This is probably not necessary, and complicates things now
+//                 that we have multiple regtest suites. Consider removing.
+func Main(m *testing.M) {
 	flag.Parse()
 	if os.Getenv("_GOPLS_TEST_BINARY_RUN_AS_GOPLS") == "true" {
 		tool.Main(context.Background(), cmd.New("gopls", "", nil, nil), os.Args[1:])
@@ -69,7 +81,7 @@
 	}
 
 	runner = &Runner{
-		DefaultModes:             NormalModes,
+		DefaultModes:             DefaultModes(),
 		Timeout:                  *regtestTimeout,
 		PrintGoroutinesOnFailure: *printGoroutinesOnFailure,
 		SkipCleanup:              *skipCleanup,
@@ -83,7 +95,6 @@
 				panic(fmt.Sprintf("finding test binary path: %v", err))
 			}
 		}
-		runner.DefaultModes = NormalModes | SeparateProcess
 		runner.GoplsPath = goplsPath
 	}
 	dir, err := ioutil.TempDir("", "gopls-regtest-")
diff --git a/gopls/internal/regtest/runner.go b/gopls/internal/regtest/runner.go
index 1e77cb8..3348040 100644
--- a/gopls/internal/regtest/runner.go
+++ b/gopls/internal/regtest/runner.go
@@ -8,7 +8,6 @@
 	"bytes"
 	"context"
 	"fmt"
-	exec "golang.org/x/sys/execabs"
 	"io"
 	"io/ioutil"
 	"net"
@@ -20,6 +19,8 @@
 	"testing"
 	"time"
 
+	exec "golang.org/x/sys/execabs"
+
 	"golang.org/x/tools/gopls/internal/hooks"
 	"golang.org/x/tools/internal/jsonrpc2"
 	"golang.org/x/tools/internal/jsonrpc2/servertest"
@@ -308,7 +309,6 @@
 	"netbsd-arm-bsiegert":     "",
 	"solaris-amd64-oraclerel": "",
 	"windows-arm-zx2c4":       "",
-	"android-amd64-emu":       "golang.org/issue/43554",
 }
 
 func checkBuilder(t *testing.T) {
diff --git a/gopls/internal/regtest/watch_test.go b/gopls/internal/regtest/watch/watch_test.go
similarity index 83%
rename from gopls/internal/regtest/watch_test.go
rename to gopls/internal/regtest/watch/watch_test.go
index 3802765..436c091 100644
--- a/gopls/internal/regtest/watch_test.go
+++ b/gopls/internal/regtest/watch/watch_test.go
@@ -7,12 +7,17 @@
 import (
 	"testing"
 
-	"golang.org/x/tools/internal/lsp"
+	. "golang.org/x/tools/gopls/internal/regtest"
+
 	"golang.org/x/tools/internal/lsp/fake"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/testenv"
 )
 
+func TestMain(m *testing.M) {
+	Main(m)
+}
+
 func TestEditFile(t *testing.T) {
 	const pkg = `
 -- go.mod --
@@ -29,7 +34,7 @@
 	// Edit the file when it's *not open* in the workspace, and check that
 	// diagnostics are updated.
 	t.Run("unopened", func(t *testing.T) {
-		runner.Run(t, pkg, func(t *testing.T, env *Env) {
+		Run(t, pkg, func(t *testing.T, env *Env) {
 			env.Await(
 				env.DiagnosticAtRegexp("a/a.go", "x"),
 			)
@@ -43,16 +48,16 @@
 	// Edit the file when it *is open* in the workspace, and check that
 	// diagnostics are *not* updated.
 	t.Run("opened", func(t *testing.T) {
-		runner.Run(t, pkg, func(t *testing.T, env *Env) {
+		Run(t, pkg, func(t *testing.T, env *Env) {
 			env.OpenFile("a/a.go")
 			// Insert a trivial edit so that we don't automatically update the buffer
 			// (see CL 267577).
 			env.EditBuffer("a/a.go", fake.NewEdit(0, 0, 0, 0, " "))
-			env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1))
+			env.Await(env.DoneWithOpen())
 			env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`)
 			env.Await(
 				OnceMet(
-					CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+					env.DoneWithChangeWatchedFiles(),
 					env.DiagnosticAtRegexp("a/a.go", "x"),
 				))
 		})
@@ -81,9 +86,9 @@
 	_ = b.B()
 }
 `
-	runner.Run(t, pkg, func(t *testing.T, env *Env) {
+	Run(t, pkg, func(t *testing.T, env *Env) {
 		env.OpenFile("a/a.go")
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1))
+		env.Await(env.DoneWithOpen())
 		env.WriteWorkspaceFile("b/b.go", `package b; func B() {};`)
 		env.Await(
 			env.DiagnosticAtRegexp("a/a.go", "b.B"),
@@ -115,7 +120,7 @@
 	_ = b.B()
 }
 `
-	runner.Run(t, pkg, func(t *testing.T, env *Env) {
+	Run(t, pkg, func(t *testing.T, env *Env) {
 		env.Await(
 			env.DiagnosticAtRegexp("a/a.go", "x"),
 		)
@@ -158,9 +163,9 @@
 	_ = b.B()
 }
 `
-	runner.Run(t, pkg, func(t *testing.T, env *Env) {
+	Run(t, pkg, func(t *testing.T, env *Env) {
 		env.OpenFile("a/a.go")
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1))
+		env.Await(env.DoneWithOpen())
 		env.RemoveWorkspaceFile("b/b.go")
 		env.Await(
 			env.DiagnosticAtRegexp("a/a.go", "\"mod.com/b\""),
@@ -190,7 +195,7 @@
 	c.C()
 }
 `
-	runner.Run(t, missing, func(t *testing.T, env *Env) {
+	Run(t, missing, func(t *testing.T, env *Env) {
 		t.Skip("the initial workspace load fails and never retries")
 
 		env.Await(
@@ -216,7 +221,7 @@
 
 func _() {}
 `
-	runner.Run(t, original, func(t *testing.T, env *Env) {
+	Run(t, original, func(t *testing.T, env *Env) {
 		env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`)
 		env.WriteWorkspaceFile("a/a.go", `package a; import "mod.com/c"; func _() { c.C() }`)
 		env.Await(
@@ -239,7 +244,7 @@
 	hello()
 }
 `
-	runner.Run(t, pkg, func(t *testing.T, env *Env) {
+	Run(t, pkg, func(t *testing.T, env *Env) {
 		env.Await(
 			env.DiagnosticAtRegexp("a/a.go", "hello"),
 		)
@@ -314,11 +319,11 @@
 
 	// Add the new method before the implementation. Expect diagnostics.
 	t.Run("method before implementation", func(t *testing.T) {
-		runner.Run(t, pkg, func(t *testing.T, env *Env) {
+		Run(t, pkg, func(t *testing.T, env *Env) {
 			env.WriteWorkspaceFile("b/b.go", newMethod)
 			env.Await(
 				OnceMet(
-					CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+					env.DoneWithChangeWatchedFiles(),
 					DiagnosticAt("a/a.go", 12, 12),
 				),
 			)
@@ -330,11 +335,11 @@
 	})
 	// Add the new implementation before the new method. Expect no diagnostics.
 	t.Run("implementation before method", func(t *testing.T) {
-		runner.Run(t, pkg, func(t *testing.T, env *Env) {
+		Run(t, pkg, func(t *testing.T, env *Env) {
 			env.WriteWorkspaceFile("a/a.go", implementation)
 			env.Await(
 				OnceMet(
-					CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+					env.DoneWithChangeWatchedFiles(),
 					NoDiagnostics("a/a.go"),
 				),
 			)
@@ -346,14 +351,14 @@
 	})
 	// Add both simultaneously. Expect no diagnostics.
 	t.Run("implementation and method simultaneously", func(t *testing.T) {
-		runner.Run(t, pkg, func(t *testing.T, env *Env) {
+		Run(t, pkg, func(t *testing.T, env *Env) {
 			env.WriteWorkspaceFiles(map[string]string{
 				"a/a.go": implementation,
 				"b/b.go": newMethod,
 			})
 			env.Await(
 				OnceMet(
-					CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+					env.DoneWithChangeWatchedFiles(),
 					NoDiagnostics("a/a.go"),
 				),
 				NoDiagnostics("b/b.go"),
@@ -380,14 +385,14 @@
 package a
 `
 	t.Run("close then delete", func(t *testing.T) {
-		withOptions(EditorConfig{
+		WithOptions(EditorConfig{
 			VerboseOutput: true,
-		}).run(t, pkg, func(t *testing.T, env *Env) {
+		}).Run(t, pkg, func(t *testing.T, env *Env) {
 			env.OpenFile("a/a.go")
 			env.OpenFile("a/a_unneeded.go")
 			env.Await(
 				OnceMet(
-					CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 2),
+					env.DoneWithOpen(),
 					LogMatching(protocol.Info, "a_unneeded.go", 1),
 				),
 			)
@@ -402,7 +407,7 @@
 			env.SaveBuffer("a/a.go")
 			env.Await(
 				OnceMet(
-					CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1),
+					env.DoneWithSave(),
 					// There should only be one log message containing
 					// a_unneeded.go, from the initial workspace load, which we
 					// check for earlier. If there are more, there's a bug.
@@ -414,14 +419,14 @@
 	})
 
 	t.Run("delete then close", func(t *testing.T) {
-		withOptions(
+		WithOptions(
 			EditorConfig{VerboseOutput: true},
-		).run(t, pkg, func(t *testing.T, env *Env) {
+		).Run(t, pkg, func(t *testing.T, env *Env) {
 			env.OpenFile("a/a.go")
 			env.OpenFile("a/a_unneeded.go")
 			env.Await(
 				OnceMet(
-					CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 2),
+					env.DoneWithOpen(),
 					LogMatching(protocol.Info, "a_unneeded.go", 1),
 				),
 			)
@@ -436,7 +441,7 @@
 			env.SaveBuffer("a/a.go")
 			env.Await(
 				OnceMet(
-					CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1),
+					env.DoneWithSave(),
 					// There should only be one log message containing
 					// a_unneeded.go, from the initial workspace load, which we
 					// check for earlier. If there are more, there's a bug.
@@ -479,7 +484,7 @@
 
 func _() {}
 `
-	runner.Run(t, pkg, func(t *testing.T, env *Env) {
+	Run(t, pkg, func(t *testing.T, env *Env) {
 		env.ChangeFilesOnDisk([]fake.FileEvent{
 			{
 				Path: "a/a3.go",
@@ -510,7 +515,7 @@
 		})
 		env.Await(
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+				env.DoneWithChangeWatchedFiles(),
 				NoDiagnostics("main.go"),
 			),
 		)
@@ -565,7 +570,7 @@
 	blah.X()
 }
 `
-	withOptions(ProxyFiles(proxy)).run(t, mod, func(t *testing.T, env *Env) {
+	WithOptions(ProxyFiles(proxy)).Run(t, mod, func(t *testing.T, env *Env) {
 		env.WriteWorkspaceFiles(map[string]string{
 			"go.mod": `module mod.com
 
@@ -585,7 +590,7 @@
 `,
 		})
 		env.Await(
-			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+			env.DoneWithChangeWatchedFiles(),
 			NoDiagnostics("main.go"),
 		)
 	})
@@ -609,10 +614,10 @@
 	_ = blah.Name
 }
 `
-	withOptions(
+	WithOptions(
 		InGOPATH(),
 		Modes(Experimental), // module is in a subdirectory
-	).run(t, files, func(t *testing.T, env *Env) {
+	).Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("foo/main.go")
 		env.Await(env.DiagnosticAtRegexp("foo/main.go", `"blah"`))
 		if err := env.Sandbox.RunGoCommand(env.Ctx, "foo", "mod", []string{"init", "mod.com"}); err != nil {
@@ -620,7 +625,7 @@
 		}
 		env.Await(
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+				env.DoneWithChangeWatchedFiles(),
 				env.DiagnosticAtRegexp("foo/main.go", `"blah"`),
 			),
 		)
@@ -653,14 +658,14 @@
 	_ = blah.Name
 }
 `
-	withOptions(
+	WithOptions(
 		InGOPATH(),
-	).run(t, files, func(t *testing.T, env *Env) {
+	).Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("foo/main.go")
 		env.RemoveWorkspaceFile("foo/go.mod")
 		env.Await(
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+				env.DoneWithChangeWatchedFiles(),
 				env.DiagnosticAtRegexp("foo/main.go", `"mod.com/blah"`),
 			),
 		)
@@ -690,7 +695,7 @@
 	bob()
 }
 `
-	run(t, files, func(t *testing.T, env *Env) {
+	Run(t, files, func(t *testing.T, env *Env) {
 		// Add a new symbol to the package under test and use it in the test
 		// variant. Expect no diagnostics.
 		env.WriteWorkspaceFiles(map[string]string{
@@ -711,11 +716,11 @@
 		})
 		env.Await(
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+				env.DoneWithChangeWatchedFiles(),
 				NoDiagnostics("a/a.go"),
 			),
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+				env.DoneWithChangeWatchedFiles(),
 				NoDiagnostics("a/a_test.go"),
 			),
 		)
@@ -743,11 +748,11 @@
 		})
 		env.Await(
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2),
+				env.DoneWithChangeWatchedFiles(),
 				NoDiagnostics("a/a_test.go"),
 			),
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2),
+				env.DoneWithChangeWatchedFiles(),
 				NoDiagnostics("a/a2_test.go"),
 			),
 		)
diff --git a/gopls/internal/regtest/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go
similarity index 86%
rename from gopls/internal/regtest/workspace_test.go
rename to gopls/internal/regtest/workspace/workspace_test.go
index f4c21f7..cad358e 100644
--- a/gopls/internal/regtest/workspace_test.go
+++ b/gopls/internal/regtest/workspace/workspace_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package regtest
+package workspace
 
 import (
 	"fmt"
@@ -12,12 +12,17 @@
 	"strings"
 	"testing"
 
-	"golang.org/x/tools/internal/lsp"
+	. "golang.org/x/tools/gopls/internal/regtest"
+
 	"golang.org/x/tools/internal/lsp/fake"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/testenv"
 )
 
+func TestMain(m *testing.M) {
+	Main(m)
+}
+
 const workspaceProxy = `
 -- example.com@v1.2.3/go.mod --
 module example.com
@@ -117,7 +122,7 @@
 			if tt.rootPath != "" {
 				opts = append(opts, WorkspaceFolders(tt.rootPath))
 			}
-			withOptions(opts...).run(t, workspaceModule, func(t *testing.T, env *Env) {
+			WithOptions(opts...).Run(t, workspaceModule, func(t *testing.T, env *Env) {
 				f := "pkg/inner/inner.go"
 				env.OpenFile(f)
 				locations := env.References(f, env.RegexpSearch(f, `SaySomething`))
@@ -135,10 +140,10 @@
 // VS Code, where clicking on a reference result triggers a
 // textDocument/didOpen without a corresponding textDocument/didClose.
 func TestClearAnalysisDiagnostics(t *testing.T) {
-	withOptions(
+	WithOptions(
 		ProxyFiles(workspaceProxy),
 		WorkspaceFolders("pkg/inner"),
-	).run(t, workspaceModule, func(t *testing.T, env *Env) {
+	).Run(t, workspaceModule, func(t *testing.T, env *Env) {
 		env.OpenFile("pkg/main.go")
 		env.Await(
 			env.DiagnosticAtRegexp("pkg/main2.go", "fmt.Print"),
@@ -153,10 +158,10 @@
 // This test checks that gopls updates the set of files it watches when a
 // replace target is added to the go.mod.
 func TestWatchReplaceTargets(t *testing.T) {
-	withOptions(
+	WithOptions(
 		ProxyFiles(workspaceProxy),
 		WorkspaceFolders("pkg"),
-	).run(t, workspaceModule, func(t *testing.T, env *Env) {
+	).Run(t, workspaceModule, func(t *testing.T, env *Env) {
 		// Add a replace directive and expect the files that gopls is watching
 		// to change.
 		dir := env.Sandbox.Workdir.URI("goodbye").SpanURI().Filename()
@@ -165,7 +170,7 @@
 `, env.ReadWorkspaceFile("pkg/go.mod"), dir)
 		env.WriteWorkspaceFile("pkg/go.mod", goModWithReplace)
 		env.Await(
-			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+			env.DoneWithChangeWatchedFiles(),
 			UnregistrationMatching("didChangeWatchedFiles"),
 			RegistrationMatching("didChangeWatchedFiles"),
 		)
@@ -199,7 +204,9 @@
 module a.com
 
 require b.com v1.2.3
-
+-- moda/a/go.sum --
+b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI=
+b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8=
 -- moda/a/a.go --
 package a
 
@@ -221,10 +228,10 @@
 	var x int
 }
 `
-	withOptions(
+	WithOptions(
 		ProxyFiles(workspaceModuleProxy),
 		Modes(Experimental),
-	).run(t, multiModule, func(t *testing.T, env *Env) {
+	).Run(t, multiModule, func(t *testing.T, env *Env) {
 		env.Await(
 			env.DiagnosticAtRegexp("moda/a/a.go", "x"),
 			env.DiagnosticAtRegexp("modb/b/b.go", "x"),
@@ -241,7 +248,9 @@
 module a.com
 
 require b.com v1.2.3
-
+-- moda/a/go.sum --
+b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI=
+b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8=
 -- moda/a/a.go --
 package a
 
@@ -263,10 +272,10 @@
 	var x int
 }
 `
-	withOptions(
+	WithOptions(
 		ProxyFiles(workspaceModuleProxy),
 		Modes(Experimental),
-	).run(t, multiModule, func(t *testing.T, env *Env) {
+	).Run(t, multiModule, func(t *testing.T, env *Env) {
 		env.OpenFile("moda/a/a.go")
 
 		original, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
@@ -277,7 +286,7 @@
 		env.RemoveWorkspaceFile("modb/b/b.go")
 		env.RemoveWorkspaceFile("modb/go.mod")
 		env.Await(
-			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2),
+			env.DoneWithChangeWatchedFiles(),
 		)
 		if testenv.Go1Point() < 14 {
 			// On 1.14 and above, the go mod tidy diagnostics accidentally
@@ -306,7 +315,9 @@
 module a.com
 
 require b.com v1.2.3
-
+-- moda/a/go.sum --
+b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI=
+b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8=
 -- moda/a/a.go --
 package a
 
@@ -319,10 +330,10 @@
 	_ = b.Hello()
 }
 `
-	withOptions(
+	WithOptions(
 		Modes(Experimental),
 		ProxyFiles(workspaceModuleProxy),
-	).run(t, multiModule, func(t *testing.T, env *Env) {
+	).Run(t, multiModule, func(t *testing.T, env *Env) {
 		env.OpenFile("moda/a/a.go")
 		original, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
 		if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(original, want) {
@@ -340,7 +351,7 @@
 		})
 		env.Await(
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+				env.DoneWithChangeWatchedFiles(),
 				env.DiagnosticAtRegexp("modb/b/b.go", "x"),
 			),
 		)
@@ -381,14 +392,14 @@
 	var x int
 }
 `
-	withOptions(
+	WithOptions(
 		ProxyFiles(workspaceModuleProxy),
 		Modes(Experimental),
-	).run(t, multiModule, func(t *testing.T, env *Env) {
+	).Run(t, multiModule, func(t *testing.T, env *Env) {
 		env.OpenFile("modb/go.mod")
 		env.Await(
 			OnceMet(
-				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1),
+				env.DoneWithOpen(),
 				DiagnosticAt("modb/go.mod", 0, 0),
 			),
 		)
@@ -409,7 +420,9 @@
 module a.com
 
 require b.com v1.2.3
-
+-- moda/a/go.sum --
+b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI=
+b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8=
 -- moda/a/a.go --
 package a
 
@@ -425,6 +438,9 @@
 module b.com
 
 require example.com v1.2.3
+-- modb/go.sum --
+example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c=
+example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo=
 -- modb/b/b.go --
 package b
 
@@ -441,10 +457,10 @@
 
 replace a.com => $SANDBOX_WORKDIR/moda/a
 `
-	withOptions(
+	WithOptions(
 		ProxyFiles(workspaceModuleProxy),
 		Modes(Experimental),
-	).run(t, multiModule, func(t *testing.T, env *Env) {
+	).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
 		// that we go to the module cache.
@@ -472,8 +488,8 @@
 		env.WriteWorkspaceFile("gopls.mod", fmt.Sprintf(`module gopls-workspace
 
 require (
-	a.com v0.0.0-goplsworkspace
-	b.com v0.0.0-goplsworkspace
+	a.com v1.9999999.0-goplsworkspace
+	b.com v1.9999999.0-goplsworkspace
 )
 
 replace a.com => %s/moda/a
@@ -488,7 +504,7 @@
 		var d protocol.PublishDiagnosticsParams
 		env.Await(
 			OnceMet(
-				env.DiagnosticAtRegexp("modb/go.mod", `require example.com v1.2.3`),
+				env.DiagnosticAtRegexpWithMessage("modb/go.mod", `require example.com v1.2.3`, "has not been downloaded"),
 				ReadDiagnostics("modb/go.mod", &d),
 			),
 		)
@@ -547,7 +563,7 @@
 import "fmt"
 var _ = fmt.Printf
 `
-	run(t, files, func(t *testing.T, env *Env) {
+	Run(t, files, func(t *testing.T, env *Env) {
 		env.CreateBuffer("/tmp/foo.go", "")
 		env.EditBuffer("/tmp/foo.go", fake.NewEdit(0, 0, 0, 0, code))
 		env.GoToDefinition("/tmp/foo.go", env.RegexpSearch("/tmp/foo.go", `Printf`))
@@ -598,9 +614,9 @@
 	var x int
 }
 `
-	withOptions(
+	WithOptions(
 		Modes(Experimental),
-	).run(t, multiModule, func(t *testing.T, env *Env) {
+	).Run(t, multiModule, func(t *testing.T, env *Env) {
 		env.Await(
 			env.DiagnosticAtRegexp("moda/a/a.go", "x"),
 			env.DiagnosticAtRegexp("modb/b/b.go", "x"),
@@ -630,10 +646,10 @@
 	fmt.Println("World")
 }
 `
-	withOptions(
+	WithOptions(
 		Modes(Experimental),
 		SendPID(),
-	).run(t, multiModule, func(t *testing.T, env *Env) {
+	).Run(t, multiModule, func(t *testing.T, env *Env) {
 		pid := os.Getpid()
 		// Don't factor this out of Server.addFolders. vscode-go expects this
 		// directory.
@@ -643,7 +659,7 @@
 			t.Fatalf("reading expected workspace modfile: %v", err)
 		}
 		got := string(gotb)
-		for _, want := range []string{"a.com v0.0.0-goplsworkspace", "b.com v0.0.0-goplsworkspace"} {
+		for _, want := range []string{"a.com v1.9999999.0-goplsworkspace", "b.com v1.9999999.0-goplsworkspace"} {
 			if !strings.Contains(got, want) {
 				// want before got here, since the go.mod is multi-line
 				t.Fatalf("workspace go.mod missing %q. got:\n%s", want, got)
@@ -654,18 +670,18 @@
 				module gopls-workspace
 
 				require (
-					a.com v0.0.0-goplsworkspace
+					a.com v1.9999999.0-goplsworkspace
 				)
 
 				replace a.com => %s/moda/a
 				`, workdir))
-		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1))
+		env.Await(env.DoneWithChangeWatchedFiles())
 		gotb, err = ioutil.ReadFile(modPath)
 		if err != nil {
 			t.Fatalf("reading expected workspace modfile: %v", err)
 		}
 		got = string(gotb)
-		want := "b.com v0.0.0-goplsworkspace"
+		want := "b.com v1.9999999.0-goplsworkspace"
 		if strings.Contains(got, want) {
 			t.Fatalf("workspace go.mod contains unexpected %q. got:\n%s", want, got)
 		}
@@ -687,7 +703,7 @@
 	cfg := EditorConfig{
 		DirectoryFilters: []string{"-exclude"},
 	}
-	withOptions(cfg).run(t, files, func(t *testing.T, env *Env) {
+	WithOptions(cfg).Run(t, files, func(t *testing.T, env *Env) {
 		env.Await(NoDiagnostics("exclude/x.go"))
 	})
 }
@@ -715,7 +731,7 @@
 	cfg := EditorConfig{
 		DirectoryFilters: []string{"-exclude"},
 	}
-	withOptions(cfg).run(t, files, func(t *testing.T, env *Env) {
+	WithOptions(cfg).Run(t, files, func(t *testing.T, env *Env) {
 		env.Await(
 			NoDiagnostics("exclude/exclude.go"), // filtered out
 			NoDiagnostics("include/include.go"), // successfully builds
@@ -765,7 +781,7 @@
 	cfg := EditorConfig{
 		DirectoryFilters: []string{"-exclude"},
 	}
-	withOptions(cfg, Modes(Experimental), ProxyFiles(proxy)).run(t, files, func(t *testing.T, env *Env) {
+	WithOptions(cfg, Modes(Experimental), ProxyFiles(proxy)).Run(t, files, func(t *testing.T, env *Env) {
 		env.Await(env.DiagnosticAtRegexp("include/include.go", `exclude.(X)`))
 	})
 }
diff --git a/gopls/internal/regtest/wrappers.go b/gopls/internal/regtest/wrappers.go
index cdc3090..bb614a5 100644
--- a/gopls/internal/regtest/wrappers.go
+++ b/gopls/internal/regtest/wrappers.go
@@ -361,7 +361,7 @@
 	return actions
 }
 
-func (e *Env) changeConfiguration(t *testing.T, config *fake.EditorConfig) {
+func (e *Env) ChangeConfiguration(t *testing.T, config *fake.EditorConfig) {
 	e.Editor.Config = *config
 	if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, &protocol.DidChangeConfigurationParams{
 		// gopls currently ignores the Settings field
diff --git a/internal/gocommand/vendor.go b/internal/gocommand/vendor.go
index 1cd8d84..5e75bd6 100644
--- a/internal/gocommand/vendor.go
+++ b/internal/gocommand/vendor.go
@@ -12,6 +12,7 @@
 	"path/filepath"
 	"regexp"
 	"strings"
+	"time"
 
 	"golang.org/x/mod/semver"
 )
@@ -19,11 +20,15 @@
 // ModuleJSON holds information about a module.
 type ModuleJSON struct {
 	Path      string      // module path
+	Version   string      // module version
+	Versions  []string    // available module versions (with -versions)
 	Replace   *ModuleJSON // replaced by this module
+	Time      *time.Time  // time version was created
+	Update    *ModuleJSON // available update, if any (with -u)
 	Main      bool        // is this the main module?
 	Indirect  bool        // is this module only an indirect dependency of main module?
 	Dir       string      // directory holding files for this module, if any
-	GoMod     string      // path to go.mod file for this module, if any
+	GoMod     string      // path to go.mod file used when loading this module, if any
 	GoVersion string      // go version used in module
 }
 
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index fb328b8..41bc79d 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -203,8 +203,8 @@
 	if !s.ValidBuildConfiguration() {
 		msg := `gopls requires a module at the root of your workspace.
 You can work with multiple modules by opening each one as a workspace folder.
-Improvements to this workflow will be coming soon (https://github.com/golang/go/issues/32394),
-and you can learn more here: https://github.com/golang/go/issues/36899.`
+Improvements to this workflow will be coming soon, and you can learn more here:
+https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`
 		return &source.CriticalError{
 			MainError: errors.Errorf(msg),
 			ErrorList: s.applyCriticalErrorToFiles(ctx, msg, openFiles),
@@ -236,13 +236,15 @@
 			msg := fmt.Sprintf(`This file is in %s, which is a nested module in the %s module.
 gopls currently requires one module per workspace folder.
 Please open %s as a separate workspace folder.
-You can learn more here: https://github.com/golang/go/issues/36899.
+You can learn more here: https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.
 `, modDir, filepath.Dir(rootModURI.Filename()), modDir)
 			srcErrs = append(srcErrs, s.applyCriticalErrorToFiles(ctx, msg, uris)...)
 		}
 		if len(srcErrs) != 0 {
 			return &source.CriticalError{
-				MainError: errors.Errorf(`You are working in a nested module. Please open it as a separate workspace folder.`),
+				MainError: errors.Errorf(`You are working in a nested module.
+Please open it as a separate workspace folder. Learn more:
+https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`),
 				ErrorList: srcErrs,
 			}
 		}
diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go
index cfd3fd1..7949918 100644
--- a/internal/lsp/cache/mod.go
+++ b/internal/lsp/cache/mod.go
@@ -6,14 +6,10 @@
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
-	"io"
-	"os"
 	"path/filepath"
 	"regexp"
 	"strings"
-	"unicode"
 
 	"golang.org/x/mod/modfile"
 	"golang.org/x/mod/module"
@@ -220,125 +216,6 @@
 	return mwh.why(ctx, s)
 }
 
-type modUpgradeHandle struct {
-	handle *memoize.Handle
-}
-
-type modUpgradeData struct {
-	// upgrades maps modules to their latest versions.
-	upgrades map[string]string
-
-	err error
-}
-
-func (muh *modUpgradeHandle) upgrades(ctx context.Context, snapshot *snapshot) (map[string]string, error) {
-	v, err := muh.handle.Get(ctx, snapshot.generation, snapshot)
-	if v == nil {
-		return nil, err
-	}
-	data := v.(*modUpgradeData)
-	return data.upgrades, data.err
-}
-
-// moduleUpgrade describes a module that can be upgraded to a particular
-// version.
-type moduleUpgrade struct {
-	Path   string
-	Update struct {
-		Version string
-	}
-}
-
-func (s *snapshot) ModUpgrade(ctx context.Context, fh source.FileHandle) (map[string]string, error) {
-	if fh.Kind() != source.Mod {
-		return nil, fmt.Errorf("%s is not a go.mod file", fh.URI())
-	}
-	if handle := s.getModUpgradeHandle(fh.URI()); handle != nil {
-		return handle.upgrades(ctx, s)
-	}
-	key := modKey{
-		sessionID: s.view.session.id,
-		env:       hashEnv(s),
-		mod:       fh.FileIdentity(),
-		view:      s.view.rootURI.Filename(),
-		verb:      upgrade,
-	}
-	h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
-		ctx, done := event.Start(ctx, "cache.ModUpgradeHandle", tag.URI.Of(fh.URI()))
-		defer done()
-
-		snapshot := arg.(*snapshot)
-
-		pm, err := snapshot.ParseMod(ctx, fh)
-		if err != nil {
-			return &modUpgradeData{err: err}
-		}
-
-		// No requires to upgrade.
-		if len(pm.File.Require) == 0 {
-			return &modUpgradeData{}
-		}
-		// Run "go list -mod readonly -u -m all" to be able to see which deps can be
-		// upgraded without modifying mod file.
-		inv := &gocommand.Invocation{
-			Verb:       "list",
-			Args:       []string{"-u", "-m", "-json", "all"},
-			WorkingDir: filepath.Dir(fh.URI().Filename()),
-		}
-		if s.workspaceMode()&tempModfile == 0 || containsVendor(fh.URI()) {
-			// Use -mod=readonly if the module contains a vendor directory
-			// (see golang/go#38711).
-			inv.ModFlag = "readonly"
-		}
-		stdout, err := snapshot.RunGoCommandDirect(ctx, source.Normal|source.AllowNetwork, inv)
-		if err != nil {
-			return &modUpgradeData{err: err}
-		}
-		var upgradeList []moduleUpgrade
-		dec := json.NewDecoder(stdout)
-		for {
-			var m moduleUpgrade
-			if err := dec.Decode(&m); err == io.EOF {
-				break
-			} else if err != nil {
-				return &modUpgradeData{err: err}
-			}
-			upgradeList = append(upgradeList, m)
-		}
-		if len(upgradeList) <= 1 {
-			return &modUpgradeData{}
-		}
-		upgrades := make(map[string]string)
-		for _, upgrade := range upgradeList[1:] {
-			if upgrade.Update.Version == "" {
-				continue
-			}
-			upgrades[upgrade.Path] = upgrade.Update.Version
-		}
-		return &modUpgradeData{
-			upgrades: upgrades,
-		}
-	}, nil)
-	muh := &modUpgradeHandle{handle: h}
-	s.mu.Lock()
-	s.modUpgradeHandles[fh.URI()] = muh
-	s.mu.Unlock()
-
-	return muh.upgrades(ctx, s)
-}
-
-// containsVendor reports whether the module has a vendor folder.
-func containsVendor(modURI span.URI) bool {
-	dir := filepath.Dir(modURI.Filename())
-	f, err := os.Stat(filepath.Join(dir, "vendor"))
-	if err != nil {
-		return false
-	}
-	return f.IsDir()
-}
-
-var moduleAtVersionRe = regexp.MustCompile(`^(?P<module>.*)@(?P<version>.*)$`)
-
 // extractGoCommandError tries to parse errors that come from the go command
 // and shape them into go.mod diagnostics.
 func (s *snapshot) extractGoCommandErrors(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, goCmdError string) []*source.Error {
@@ -356,6 +233,8 @@
 	return srcErrs
 }
 
+var moduleVersionInErrorRe = regexp.MustCompile(`[:\s]([+-._~0-9A-Za-z]+)@([+-._~0-9A-Za-z]+)[:\s]`)
+
 // matchErrorToModule attempts to match module version in error messages.
 // Some examples:
 //
@@ -363,99 +242,99 @@
 //    go: github.com/cockroachdb/apd/v2@v2.0.72: reading github.com/cockroachdb/apd/go.mod at revision v2.0.72: unknown revision v2.0.72
 //    go: example.com@v1.2.3 requires\n\trandom.org@v1.2.3: parsing go.mod:\n\tmodule declares its path as: bob.org\n\tbut was required as: random.org
 //
-// We split on colons and whitespace, and attempt to match on something
-// that matches module@version. If we're able to find a match, we try to
-// find anything that matches it in the go.mod file.
+// We search for module@version, starting from the end to find the most
+// relevant module, e.g. random.org@v1.2.3 above. Then we associate the error
+// with a directive that references any of the modules mentioned.
 func (s *snapshot) matchErrorToModule(ctx context.Context, fh source.FileHandle, goCmdError string) *source.Error {
-	var v module.Version
-	fields := strings.FieldsFunc(goCmdError, func(r rune) bool {
-		return unicode.IsSpace(r) || r == ':'
-	})
-	for _, field := range fields {
-		match := moduleAtVersionRe.FindStringSubmatch(field)
-		if match == nil {
-			continue
-		}
-		path, version := match[1], match[2]
-		// Any module versions that come from the workspace module should not
-		// be shown to the user.
-		if source.IsWorkspaceModuleVersion(version) {
-			continue
-		}
-		if err := module.Check(path, version); err != nil {
-			continue
-		}
-		v.Path, v.Version = path, version
-		break
-	}
 	pm, err := s.ParseMod(ctx, fh)
 	if err != nil {
 		return nil
 	}
-	toSourceError := func(line *modfile.Line) *source.Error {
-		rng, err := rangeFromPositions(pm.Mapper, line.Start, line.End)
+
+	var innermost *module.Version
+	var reference *modfile.Line
+	matches := moduleVersionInErrorRe.FindAllStringSubmatch(goCmdError, -1)
+
+outer:
+	for i := len(matches) - 1; i >= 0; i-- {
+		ver := module.Version{Path: matches[i][1], Version: matches[i][2]}
+		// Any module versions that come from the workspace module should not
+		// be shown to the user.
+		if source.IsWorkspaceModuleVersion(ver.Version) {
+			continue
+		}
+		if err := module.Check(ver.Path, ver.Version); err != nil {
+			continue
+		}
+		if innermost == nil {
+			innermost = &ver
+		}
+
+		for _, req := range pm.File.Require {
+			if req.Mod == ver {
+				reference = req.Syntax
+				break outer
+			}
+		}
+		for _, ex := range pm.File.Exclude {
+			if ex.Mod == ver {
+				reference = ex.Syntax
+				break outer
+			}
+		}
+		for _, rep := range pm.File.Replace {
+			if rep.New == ver || rep.Old == ver {
+				reference = rep.Syntax
+				break outer
+			}
+		}
+	}
+
+	if reference == nil {
+		// No match for the module path was found in the go.mod file.
+		// Show the error on the module declaration, if one exists.
+		if pm.File.Module == nil {
+			return nil
+		}
+		reference = pm.File.Module.Syntax
+	}
+
+	rng, err := rangeFromPositions(pm.Mapper, reference.Start, reference.End)
+	if err != nil {
+		return nil
+	}
+	disabledByGOPROXY := strings.Contains(goCmdError, "disabled by GOPROXY=off")
+	shouldAddDep := strings.Contains(goCmdError, "to add it")
+	if innermost != nil && (disabledByGOPROXY || shouldAddDep) {
+		args, err := source.MarshalArgs(fh.URI(), false, []string{fmt.Sprintf("%v@%v", innermost.Path, innermost.Version)})
 		if err != nil {
 			return nil
 		}
-		disabledByGOPROXY := strings.Contains(goCmdError, "disabled by GOPROXY=off")
-		shouldAddDep := strings.Contains(goCmdError, "to add it")
-		if v.Path != "" && (disabledByGOPROXY || shouldAddDep) {
-			args, err := source.MarshalArgs(fh.URI(), false, []string{fmt.Sprintf("%v@%v", v.Path, v.Version)})
-			if err != nil {
-				return nil
-			}
-			msg := goCmdError
-			if disabledByGOPROXY {
-				msg = fmt.Sprintf("%v@%v has not been downloaded", v.Path, v.Version)
-			}
-			return &source.Error{
-				Message: msg,
-				Kind:    source.ListError,
-				Range:   rng,
-				URI:     fh.URI(),
-				SuggestedFixes: []source.SuggestedFix{{
-					Title: fmt.Sprintf("Download %v@%v", v.Path, v.Version),
-					Command: &protocol.Command{
-						Title:     source.CommandAddDependency.Title,
-						Command:   source.CommandAddDependency.ID(),
-						Arguments: args,
-					},
-				}},
-			}
+		msg := goCmdError
+		if disabledByGOPROXY {
+			msg = fmt.Sprintf("%v@%v has not been downloaded", innermost.Path, innermost.Version)
 		}
 		return &source.Error{
-			Message: goCmdError,
+			Message: msg,
+			Kind:    source.ListError,
 			Range:   rng,
 			URI:     fh.URI(),
-			Kind:    source.ListError,
+			SuggestedFixes: []source.SuggestedFix{{
+				Title: fmt.Sprintf("Download %v@%v", innermost.Path, innermost.Version),
+				Command: &protocol.Command{
+					Title:     source.CommandAddDependency.Title,
+					Command:   source.CommandAddDependency.ID(),
+					Arguments: args,
+				},
+			}},
 		}
 	}
-	// Check if there are any require, exclude, or replace statements that
-	// match this module version.
-	for _, req := range pm.File.Require {
-		if req.Mod != v {
-			continue
-		}
-		return toSourceError(req.Syntax)
+	return &source.Error{
+		Message: goCmdError,
+		Range:   rng,
+		URI:     fh.URI(),
+		Kind:    source.ListError,
 	}
-	for _, ex := range pm.File.Exclude {
-		if ex.Mod != v {
-			continue
-		}
-		return toSourceError(ex.Syntax)
-	}
-	for _, rep := range pm.File.Replace {
-		if rep.New != v && rep.Old != v {
-			continue
-		}
-		return toSourceError(rep.Syntax)
-	}
-	// No match for the module path was found in the go.mod file.
-	// Show the error on the module declaration, if one exists.
-	if pm.File.Module == nil {
-		return nil
-	}
-	return toSourceError(pm.File.Module.Syntax)
 }
 
 // errorPositionRe matches errors messages of the form <filename>:<line>:<col>,
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 552d1c5..1102808 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -200,8 +200,9 @@
 		baseCtx:              baseCtx,
 		name:                 name,
 		folder:               folder,
-		filesByURI:           make(map[span.URI]*fileBase),
-		filesByBase:          make(map[string][]*fileBase),
+		moduleUpgrades:       map[string]string{},
+		filesByURI:           map[span.URI]*fileBase{},
+		filesByBase:          map[string][]*fileBase{},
 		rootURI:              root,
 		workspaceInformation: *ws,
 		tempWorkspace:        tempWorkspace,
@@ -230,7 +231,6 @@
 		unloadableFiles:   make(map[span.URI]struct{}),
 		parseModHandles:   make(map[span.URI]*parseModHandle),
 		modTidyHandles:    make(map[span.URI]*modTidyHandle),
-		modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
 		modWhyHandles:     make(map[span.URI]*modWhyHandle),
 		workspace:         workspace,
 	}
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 08459a7..7bea58e 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -21,6 +21,7 @@
 
 	"golang.org/x/mod/modfile"
 	"golang.org/x/mod/module"
+	"golang.org/x/mod/semver"
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/event"
@@ -103,9 +104,8 @@
 	// Preserve go.mod-related handles to avoid garbage-collecting the results
 	// of various calls to the go command. The handles need not refer to only
 	// the view's go.mod file.
-	modTidyHandles    map[span.URI]*modTidyHandle
-	modUpgradeHandles map[span.URI]*modUpgradeHandle
-	modWhyHandles     map[span.URI]*modWhyHandle
+	modTidyHandles map[span.URI]*modTidyHandle
+	modWhyHandles  map[span.URI]*modWhyHandle
 
 	workspace          *workspace
 	workspaceDirHandle *memoize.Handle
@@ -324,12 +324,8 @@
 	case source.LoadWorkspace, source.Normal:
 		if vendorEnabled {
 			inv.ModFlag = "vendor"
-		} else if s.workspaceMode()&usesWorkspaceModule == 0 && !allowModfileModificationOption {
+		} else if !allowModfileModificationOption {
 			inv.ModFlag = "readonly"
-		} else {
-			// Temporarily allow updates for multi-module workspace mode:
-			// it doesn't create a go.sum at all. golang/go#42509.
-			inv.ModFlag = mutableModFlag
 		}
 	case source.UpdateUserModFile, source.WriteTemporaryModFile:
 		inv.ModFlag = mutableModFlag
@@ -574,12 +570,6 @@
 	return s.modWhyHandles[uri]
 }
 
-func (s *snapshot) getModUpgradeHandle(uri span.URI) *modUpgradeHandle {
-	s.mu.Lock()
-	defer s.mu.Unlock()
-	return s.modUpgradeHandles[uri]
-}
-
 func (s *snapshot) getModTidyHandle(uri span.URI) *modTidyHandle {
 	s.mu.Lock()
 	defer s.mu.Unlock()
@@ -717,6 +707,9 @@
 // the given directory. It does not respect symlinks.
 func (s *snapshot) knownFilesInDir(ctx context.Context, dir span.URI) []span.URI {
 	var files []span.URI
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
 	for uri := range s.files {
 		if source.InDir(dir.Filename(), uri.Filename()) {
 			files = append(files, uri)
@@ -1009,6 +1002,9 @@
 
 func (s *snapshot) GetCriticalError(ctx context.Context) *source.CriticalError {
 	loadErr := s.awaitLoadedAllErrors(ctx)
+	if errors.Is(loadErr, context.Canceled) {
+		return nil
+	}
 
 	// Even if packages didn't fail to load, we still may want to show
 	// additional warnings.
@@ -1075,6 +1071,10 @@
 	// Do not return results until the snapshot's view has been initialized.
 	s.AwaitInitialized(ctx)
 
+	if ctx.Err() != nil {
+		return ctx.Err()
+	}
+
 	if err := s.reloadWorkspace(ctx); err != nil {
 		return err
 	}
@@ -1286,7 +1286,6 @@
 		unloadableFiles:   make(map[span.URI]struct{}),
 		parseModHandles:   make(map[span.URI]*parseModHandle),
 		modTidyHandles:    make(map[span.URI]*modTidyHandle),
-		modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
 		modWhyHandles:     make(map[span.URI]*modWhyHandle),
 		workspace:         newWorkspace,
 	}
@@ -1331,12 +1330,6 @@
 		}
 		result.modTidyHandles[k] = v
 	}
-	for k, v := range s.modUpgradeHandles {
-		if _, ok := changes[k]; ok {
-			continue
-		}
-		result.modUpgradeHandles[k] = v
-	}
 	for k, v := range s.modWhyHandles {
 		if _, ok := changes[k]; ok {
 			continue
@@ -1390,9 +1383,6 @@
 			for k := range s.modTidyHandles {
 				delete(result.modTidyHandles, k)
 			}
-			for k := range s.modUpgradeHandles {
-				delete(result.modUpgradeHandles, k)
-			}
 			for k := range s.modWhyHandles {
 				delete(result.modWhyHandles, k)
 			}
@@ -1521,9 +1511,6 @@
 	for _, v := range result.modTidyHandles {
 		newGen.Inherit(v.handle)
 	}
-	for _, v := range result.modUpgradeHandles {
-		newGen.Inherit(v.handle)
-	}
 	for _, v := range result.modWhyHandles {
 		newGen.Inherit(v.handle)
 	}
@@ -1731,6 +1718,9 @@
 func buildWorkspaceModFile(ctx context.Context, modFiles map[span.URI]struct{}, fs source.FileSource) (*modfile.File, error) {
 	file := &modfile.File{}
 	file.AddModuleStmt("gopls-workspace")
+	// Track the highest Go version, to be set on the workspace module.
+	// Fall back to 1.12 -- old versions insist on having some version.
+	goVersion := "1.12"
 
 	paths := make(map[string]span.URI)
 	for modURI := range modFiles {
@@ -1749,7 +1739,13 @@
 		if file == nil || parsed.Module == nil {
 			return nil, fmt.Errorf("no module declaration for %s", modURI)
 		}
+		if parsed.Go != nil && semver.Compare(goVersion, parsed.Go.Version) < 0 {
+			goVersion = parsed.Go.Version
+		}
 		path := parsed.Module.Mod.Path
+		if _, ok := paths[path]; ok {
+			return nil, fmt.Errorf("found module %q twice in the workspace", path)
+		}
 		paths[path] = modURI
 		// If the module's path includes a major version, we expect it to have
 		// a matching major version.
@@ -1763,6 +1759,9 @@
 			return nil, err
 		}
 	}
+	if goVersion != "" {
+		file.AddGoStmt(goVersion)
+	}
 	// Go back through all of the modules to handle any of their replace
 	// statements.
 	for modURI := range modFiles {
@@ -1802,6 +1801,7 @@
 			}
 		}
 	}
+	file.SortBlocks()
 	return file, nil
 }
 
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index ec35561..5b767d1 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -60,6 +60,9 @@
 
 	importsState *importsState
 
+	// moduleUpgrades tracks known upgrades for module paths.
+	moduleUpgrades map[string]string
+
 	// keep track of files by uri and by basename, a single file may be mapped
 	// to multiple uris, and the same basename may map to multiple files
 	filesByURI  map[span.URI]*fileBase
@@ -863,6 +866,26 @@
 	return globsMatchPath(v.goprivate, target)
 }
 
+func (v *View) ModuleUpgrades() map[string]string {
+	v.mu.Lock()
+	defer v.mu.Unlock()
+
+	upgrades := map[string]string{}
+	for mod, ver := range v.moduleUpgrades {
+		upgrades[mod] = ver
+	}
+	return upgrades
+}
+
+func (v *View) RegisterModuleUpgrades(upgrades map[string]string) {
+	v.mu.Lock()
+	defer v.mu.Unlock()
+
+	for mod, ver := range upgrades {
+		v.moduleUpgrades[mod] = ver
+	}
+}
+
 // Copied from
 // https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/str/path.go;l=58;drc=2910c5b4a01a573ebc97744890a07c1a3122c67a
 func globsMatchPath(globs, target string) bool {
diff --git a/internal/lsp/cmd/call_hierarchy.go b/internal/lsp/cmd/call_hierarchy.go
index 48b4c46..2f870f0 100644
--- a/internal/lsp/cmd/call_hierarchy.go
+++ b/internal/lsp/cmd/call_hierarchy.go
@@ -30,8 +30,6 @@
   $ # 1-indexed location (:line:column or :#offset) of the target identifier
   $ gopls call_hierarchy helper/helper.go:8:6
   $ gopls call_hierarchy helper/helper.go:#53
-
-  gopls call_hierarchy flags are:
 `)
 	f.PrintDefaults()
 }
diff --git a/internal/lsp/cmd/check.go b/internal/lsp/cmd/check.go
index 7d22db8..42d1976 100644
--- a/internal/lsp/cmd/check.go
+++ b/internal/lsp/cmd/check.go
@@ -26,8 +26,6 @@
 Example: show the diagnostic results of this file:
 
   $ gopls check internal/lsp/cmd/check.go
-
-	gopls check flags are:
 `)
 	f.PrintDefaults()
 }
diff --git a/internal/lsp/cmd/highlight.go b/internal/lsp/cmd/highlight.go
index f2f3806..b60d513 100644
--- a/internal/lsp/cmd/highlight.go
+++ b/internal/lsp/cmd/highlight.go
@@ -30,8 +30,6 @@
   $ # 1-indexed location (:line:column or :#offset) of the target identifier
   $ gopls highlight helper/helper.go:8:6
   $ gopls highlight helper/helper.go:#53
-
-  gopls highlight flags are:
 `)
 	f.PrintDefaults()
 }
diff --git a/internal/lsp/cmd/implementation.go b/internal/lsp/cmd/implementation.go
index e498372..18eaa4ed 100644
--- a/internal/lsp/cmd/implementation.go
+++ b/internal/lsp/cmd/implementation.go
@@ -30,8 +30,6 @@
   $ # 1-indexed location (:line:column or :#offset) of the target identifier
   $ gopls implementation helper/helper.go:8:6
   $ gopls implementation helper/helper.go:#53
-
-  gopls implementation flags are:
 `)
 	f.PrintDefaults()
 }
diff --git a/internal/lsp/cmd/prepare_rename.go b/internal/lsp/cmd/prepare_rename.go
index 2a2fffe..2e6965e 100644
--- a/internal/lsp/cmd/prepare_rename.go
+++ b/internal/lsp/cmd/prepare_rename.go
@@ -30,8 +30,6 @@
 	$ # 1-indexed location (:line:column or :#offset) of the target identifier
 	$ gopls prepare_rename helper/helper.go:8:6
 	$ gopls prepare_rename helper/helper.go:#53
-
-	gopls prepare_rename flags are:
 `)
 	f.PrintDefaults()
 }
diff --git a/internal/lsp/cmd/semantictokens.go b/internal/lsp/cmd/semantictokens.go
index 7716d14..cf7d431 100644
--- a/internal/lsp/cmd/semantictokens.go
+++ b/internal/lsp/cmd/semantictokens.go
@@ -67,8 +67,6 @@
 Example: show the semantic tokens for this file:
 
   $ gopls semtok internal/lsp/cmd/semtok.go
-
-	gopls semtok flags are:
 `)
 	f.PrintDefaults()
 }
diff --git a/internal/lsp/cmd/signature.go b/internal/lsp/cmd/signature.go
index c628f38..0a7a599 100644
--- a/internal/lsp/cmd/signature.go
+++ b/internal/lsp/cmd/signature.go
@@ -29,8 +29,6 @@
   $ # 1-indexed location (:line:column or :#offset) of the target identifier
   $ gopls signature helper/helper.go:8:6
   $ gopls signature helper/helper.go:#53
-
-  gopls signature flags are:
 `)
 	f.PrintDefaults()
 }
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index 79c8663..a3bc21d 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -217,6 +217,25 @@
 			return err
 		}
 		return runSimpleGoCommand(ctx, snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "list", []string{"all"})
+	case source.CommandCheckUpgrades:
+		var uri protocol.DocumentURI
+		var modules []string
+		if err := source.UnmarshalArgs(args, &uri, &modules); err != nil {
+			return err
+		}
+		snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
+		defer release()
+		if !ok {
+			return err
+		}
+		upgrades, err := s.getUpgrades(ctx, snapshot, uri.SpanURI(), modules)
+		if err != nil {
+			return err
+		}
+		snapshot.View().RegisterModuleUpgrades(upgrades)
+		// Re-diagnose the snapshot to publish the new module diagnostics.
+		s.diagnoseSnapshot(snapshot, nil, false)
+		return nil
 	case source.CommandAddDependency, source.CommandUpgradeDependency:
 		var uri protocol.DocumentURI
 		var goCmdArgs []string
@@ -511,3 +530,27 @@
 	})
 	return err
 }
+
+func (s *Server) getUpgrades(ctx context.Context, snapshot source.Snapshot, uri span.URI, modules []string) (map[string]string, error) {
+	stdout, err := snapshot.RunGoCommandDirect(ctx, source.Normal|source.AllowNetwork, &gocommand.Invocation{
+		Verb:       "list",
+		Args:       append([]string{"-m", "-u", "-json"}, modules...),
+		WorkingDir: filepath.Dir(uri.Filename()),
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	upgrades := map[string]string{}
+	for dec := json.NewDecoder(stdout); dec.More(); {
+		mod := &gocommand.ModuleJSON{}
+		if err := dec.Decode(mod); err != nil {
+			return nil, err
+		}
+		if mod.Update == nil {
+			continue
+		}
+		upgrades[mod.Path] = mod.Update.Version
+	}
+	return upgrades, nil
+}
diff --git a/internal/lsp/debug/info.go b/internal/lsp/debug/info.go
index 9027b00..8aba552 100644
--- a/internal/lsp/debug/info.go
+++ b/internal/lsp/debug/info.go
@@ -26,7 +26,7 @@
 )
 
 // Version is a manually-updated mechanism for tracking versions.
-const Version = "v0.6.4"
+const Version = "v0.6.5"
 
 // ServerVersion is the format used by gopls to report its version to the
 // client. This format is structured so that the client can parse it easily.
diff --git a/internal/lsp/debug/serve.go b/internal/lsp/debug/serve.go
index aec976a..473518e 100644
--- a/internal/lsp/debug/serve.go
+++ b/internal/lsp/debug/serve.go
@@ -181,6 +181,7 @@
 	Logfile      string
 	GoplsPath    string
 	ServerID     string
+	Service      protocol.Server
 }
 
 // A Server is an outgoing connection to a remote LSP server.
@@ -314,6 +315,16 @@
 	return template.HTML(buf.String())
 }
 
+func (i *Instance) AddService(s protocol.Server, session *cache.Session) {
+	for _, c := range i.State.clients {
+		if c.Session == session {
+			c.Service = s
+			return
+		}
+	}
+	stdlog.Printf("unable to find a Client to add the protocol.Server to")
+}
+
 func getMemory(r *http.Request) interface{} {
 	var m runtime.MemStats
 	runtime.ReadMemStats(&m)
@@ -785,6 +796,29 @@
 {{if .DebugAddress}}Debug this client at: <a href="http://{{localAddress .DebugAddress}}">{{localAddress .DebugAddress}}</a><br>{{end}}
 Logfile: {{.Logfile}}<br>
 Gopls Path: {{.GoplsPath}}<br>
+<h2>Diagnostics</h2>
+{{/*Service: []protocol.Server; each server has map[uri]fileReports;
+	each fileReport: map[diagnosticSoure]diagnosticReport
+	diagnosticSource is one of 5 source
+	diagnosticReport: snapshotID and map[hash]*source.Diagnostic
+	sourceDiagnostic: struct {
+		Range    protocol.Range
+		Message  string
+		Source   string
+		Code     string
+		CodeHref string
+		Severity protocol.DiagnosticSeverity
+		Tags     []protocol.DiagnosticTag
+
+		Related []RelatedInformation
+	}
+	RelatedInformation: struct {
+		URI     span.URI
+		Range   protocol.Range
+		Message string
+	}
+	*/}}
+<ul>{{range $k, $v := .Service.Diagnostics}}<li>{{$k}}:<ol>{{range $v}}<li>{{.}}</li>{{end}}</ol></li>{{end}}</ul>
 {{end}}
 `))
 
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 4941f00..adf19ad 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -50,6 +50,23 @@
 	reports       map[diagnosticSource]diagnosticReport
 }
 
+func (d diagnosticSource) String() string {
+	switch d {
+	case modSource:
+		return "FromSource"
+	case gcDetailsSource:
+		return "FromGCDetails"
+	case analysisSource:
+		return "FromAnalysis"
+	case typeCheckSource:
+		return "FromTypeChecking"
+	case orphanedSource:
+		return "FromOrphans"
+	default:
+		return fmt.Sprintf("From?%d?", d)
+	}
+}
+
 // hashDiagnostics computes a hash to identify diags.
 func hashDiagnostics(diags ...*source.Diagnostic) string {
 	source.SortDiagnostics(diags)
@@ -546,3 +563,34 @@
 	})
 	return !hasGo
 }
+
+// Diagnostics formattedfor the debug server
+// (all the relevant fields of Server are private)
+// (The alternative is to export them)
+func (s *Server) Diagnostics() map[string][]string {
+	ans := make(map[string][]string)
+	s.diagnosticsMu.Lock()
+	defer s.diagnosticsMu.Unlock()
+	for k, v := range s.diagnostics {
+		fn := k.Filename()
+		for typ, d := range v.reports {
+			if len(d.diags) == 0 {
+				continue
+			}
+			for _, dx := range d.diags {
+				ans[fn] = append(ans[fn], auxStr(dx, d, typ))
+			}
+		}
+	}
+	return ans
+}
+
+func auxStr(v *source.Diagnostic, d diagnosticReport, typ diagnosticSource) string {
+	// Tags? RelatedInformation?
+	msg := fmt.Sprintf("(%s)%q(source:%q,code:%q,severity:%s,snapshot:%d,type:%s)",
+		v.Range, v.Message, v.Source, v.Code, v.Severity, d.snapshotID, typ)
+	for _, r := range v.Related {
+		msg += fmt.Sprintf(" [%s:%s,%q]", r.URI.Filename(), r.Range, r.Message)
+	}
+	return msg
+}
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
index 758b64b..3df5b24 100644
--- a/internal/lsp/fake/editor.go
+++ b/internal/lsp/fake/editor.go
@@ -41,14 +41,17 @@
 	buffers map[string]buffer
 	// Capabilities / Options
 	serverCapabilities protocol.ServerCapabilities
+
 	// Call metrics for the purpose of expectations. This is done in an ad-hoc
 	// manner for now. Perhaps in the future we should do something more
-	// systematic.
-	calls CallCounts
+	// systematic. Guarded with a separate mutex as calls may need to be accessed
+	// asynchronously via callbacks into the Editor.
+	callsMu sync.Mutex
+	calls   CallCounts
 }
 
 type CallCounts struct {
-	DidOpen, DidChange, DidChangeWatchedFiles int
+	DidOpen, DidChange, DidSave, DidChangeWatchedFiles, DidClose uint64
 }
 
 type buffer struct {
@@ -140,8 +143,8 @@
 }
 
 func (e *Editor) Stats() CallCounts {
-	e.mu.Lock()
-	defer e.mu.Unlock()
+	e.callsMu.Lock()
+	defer e.callsMu.Unlock()
 	return e.calls
 }
 
@@ -291,36 +294,48 @@
 	return nil
 }
 
+// onFileChanges is registered to be called by the Workdir on any writes that
+// go through the Workdir API. It is called synchronously by the Workdir.
 func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) {
 	if e.Server == nil {
 		return
 	}
-	e.mu.Lock()
-	defer e.mu.Unlock()
-	var lspevts []protocol.FileEvent
-	for _, evt := range evts {
-		// Always send an on-disk change, even for events that seem useless
-		// because they're shadowed by an open buffer.
-		lspevts = append(lspevts, evt.ProtocolEvent)
 
-		if buf, ok := e.buffers[evt.Path]; ok {
-			// Following VS Code, don't honor deletions or changes to dirty buffers.
-			if buf.dirty || evt.ProtocolEvent.Type == protocol.Deleted {
-				continue
-			}
-
-			content, err := e.sandbox.Workdir.ReadFile(evt.Path)
-			if err != nil {
-				continue // A race with some other operation.
-			}
-			// During shutdown, this call will fail. Ignore the error.
-			_ = e.setBufferContentLocked(ctx, evt.Path, false, strings.Split(content, "\n"), nil)
-		}
-	}
-	e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{
-		Changes: lspevts,
-	})
+	// e may be locked when onFileChanges is called, but it is important that we
+	// synchronously increment this counter so that we can subsequently assert on
+	// the number of expected DidChangeWatchedFiles calls.
+	e.callsMu.Lock()
 	e.calls.DidChangeWatchedFiles++
+	e.callsMu.Unlock()
+
+	// Since e may be locked, we must run this mutation asynchronously.
+	go func() {
+		e.mu.Lock()
+		defer e.mu.Unlock()
+		var lspevts []protocol.FileEvent
+		for _, evt := range evts {
+			// Always send an on-disk change, even for events that seem useless
+			// because they're shadowed by an open buffer.
+			lspevts = append(lspevts, evt.ProtocolEvent)
+
+			if buf, ok := e.buffers[evt.Path]; ok {
+				// Following VS Code, don't honor deletions or changes to dirty buffers.
+				if buf.dirty || evt.ProtocolEvent.Type == protocol.Deleted {
+					continue
+				}
+
+				content, err := e.sandbox.Workdir.ReadFile(evt.Path)
+				if err != nil {
+					continue // A race with some other operation.
+				}
+				// During shutdown, this call will fail. Ignore the error.
+				_ = e.setBufferContentLocked(ctx, evt.Path, false, strings.Split(content, "\n"), nil)
+			}
+		}
+		e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{
+			Changes: lspevts,
+		})
+	}()
 }
 
 // OpenFile creates a buffer for the given workdir-relative file.
@@ -371,7 +386,9 @@
 		}); err != nil {
 			return errors.Errorf("DidOpen: %w", err)
 		}
+		e.callsMu.Lock()
 		e.calls.DidOpen++
+		e.callsMu.Unlock()
 	}
 	return nil
 }
@@ -393,6 +410,9 @@
 		}); err != nil {
 			return errors.Errorf("DidClose: %w", err)
 		}
+		e.callsMu.Lock()
+		e.calls.DidClose++
+		e.callsMu.Unlock()
 	}
 	return nil
 }
@@ -458,6 +478,9 @@
 		if err := e.Server.DidSave(ctx, params); err != nil {
 			return errors.Errorf("DidSave: %w", err)
 		}
+		e.callsMu.Lock()
+		e.calls.DidSave++
+		e.callsMu.Unlock()
 	}
 	return nil
 }
@@ -653,7 +676,9 @@
 		if err := e.Server.DidChange(ctx, params); err != nil {
 			return errors.Errorf("DidChange: %w", err)
 		}
+		e.callsMu.Lock()
 		e.calls.DidChange++
+		e.callsMu.Unlock()
 	}
 	return nil
 }
diff --git a/internal/lsp/fake/workdir.go b/internal/lsp/fake/workdir.go
index 3cc6f73..5103bdb 100644
--- a/internal/lsp/fake/workdir.go
+++ b/internal/lsp/fake/workdir.go
@@ -211,7 +211,7 @@
 	copy(watchers, w.watchers)
 	w.watcherMu.Unlock()
 	for _, w := range watchers {
-		go w(ctx, evts)
+		w(ctx, evts)
 	}
 }
 
diff --git a/internal/lsp/fake/workdir_test.go b/internal/lsp/fake/workdir_test.go
index 5c9a36c..f57ea37 100644
--- a/internal/lsp/fake/workdir_test.go
+++ b/internal/lsp/fake/workdir_test.go
@@ -41,7 +41,9 @@
 
 	fileEvents := make(chan []FileEvent)
 	watch := func(_ context.Context, events []FileEvent) {
-		fileEvents <- events
+		go func() {
+			fileEvents <- events
+		}()
 	}
 	wd.AddWatcher(watch)
 	return wd, fileEvents, cleanup
diff --git a/internal/lsp/lsprpc/lsprpc.go b/internal/lsp/lsprpc/lsprpc.go
index 623533f..5126791 100644
--- a/internal/lsp/lsprpc/lsprpc.go
+++ b/internal/lsp/lsprpc/lsprpc.go
@@ -61,6 +61,7 @@
 	server := s.serverForTest
 	if server == nil {
 		server = lsp.NewServer(session, client)
+		debug.GetInstance(ctx).AddService(server, session)
 	}
 	// Clients may or may not send a shutdown message. Make sure the server is
 	// shut down.
diff --git a/internal/lsp/mod/code_lens.go b/internal/lsp/mod/code_lens.go
index 4465637..88ffe84 100644
--- a/internal/lsp/mod/code_lens.go
+++ b/internal/lsp/mod/code_lens.go
@@ -42,6 +42,10 @@
 	for _, req := range pm.File.Require {
 		requires = append(requires, req.Mod.Path)
 	}
+	checkUpgradeArgs, err := source.MarshalArgs(fh.URI(), requires)
+	if err != nil {
+		return nil, err
+	}
 	upgradeDirectArgs, err := source.MarshalArgs(fh.URI(), false, requires)
 	if err != nil {
 		return nil, err
@@ -51,10 +55,19 @@
 	if err != nil {
 		return nil, err
 	}
+
 	return []protocol.CodeLens{
 		{
 			Range: rng,
 			Command: protocol.Command{
+				Title:     "Check for upgrades",
+				Command:   source.CommandCheckUpgrades.ID(),
+				Arguments: checkUpgradeArgs,
+			},
+		},
+		{
+			Range: rng,
+			Command: protocol.Command{
 				Title:     "Upgrade transitive dependencies",
 				Command:   source.CommandUpgradeDependency.ID(),
 				Arguments: upgradeTransitiveArgs,
@@ -69,7 +82,6 @@
 			},
 		},
 	}, nil
-
 }
 
 func tidyLens(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) {
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index f57a743..b5da560 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -8,6 +8,7 @@
 
 import (
 	"context"
+	"fmt"
 
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/lsp/debug/tag"
@@ -36,9 +37,12 @@
 				Range:   e.Range,
 				Source:  e.Category,
 			}
-			if e.Category == "syntax" || e.Kind == source.ListError {
+			switch {
+			case e.Category == "syntax", e.Kind == source.ListError:
 				d.Severity = protocol.SeverityError
-			} else {
+			case e.Kind == source.UpgradeNotification:
+				d.Severity = protocol.SeverityInformation
+			default:
 				d.Severity = protocol.SeverityWarning
 			}
 			fh, err := snapshot.GetVersionedFile(ctx, e.URI)
@@ -59,13 +63,49 @@
 		}
 		return pm.ParseErrors, nil
 	}
+
+	var errors []*source.Error
+
+	// Add upgrade quick fixes for individual modules if we know about them.
+	upgrades := snapshot.View().ModuleUpgrades()
+	for _, req := range pm.File.Require {
+		ver, ok := upgrades[req.Mod.Path]
+		if !ok || req.Mod.Version == ver {
+			continue
+		}
+		rng, err := lineToRange(pm.Mapper, fh.URI(), req.Syntax.Start, req.Syntax.End)
+		if err != nil {
+			return nil, err
+		}
+		// Upgrade to the exact version we offer the user, not the most recent.
+		args, err := source.MarshalArgs(fh.URI(), false, []string{req.Mod.Path + "@" + ver})
+		if err != nil {
+			return nil, err
+		}
+		errors = append(errors, &source.Error{
+			URI:     fh.URI(),
+			Range:   rng,
+			Kind:    source.UpgradeNotification,
+			Message: fmt.Sprintf("%v can be upgraded", req.Mod.Path),
+			SuggestedFixes: []source.SuggestedFix{{
+				Title: fmt.Sprintf("Upgrade to %v", ver),
+				Command: &protocol.Command{
+					Title:     fmt.Sprintf("Upgrade to %v", ver),
+					Command:   source.CommandUpgradeDependency.ID(),
+					Arguments: args,
+				},
+			}},
+		})
+	}
+
 	tidied, err := snapshot.ModTidy(ctx, pm)
 
 	if source.IsNonFatalGoModError(err) {
-		return nil, nil
+		return errors, nil
 	}
 	if err != nil {
 		return nil, err
 	}
-	return tidied.Errors, nil
+	errors = append(errors, tidied.Errors...)
+	return errors, nil
 }
diff --git a/internal/lsp/protocol/tsprotocol.go b/internal/lsp/protocol/tsprotocol.go
index 09c1934..340fae6 100644
--- a/internal/lsp/protocol/tsprotocol.go
+++ b/internal/lsp/protocol/tsprotocol.go
@@ -285,7 +285,7 @@
 	 *
 	 * @since 3.16.0
 	 */
-	Disabled struct {
+	Disabled *struct {
 		/**
 		 * Human readable description of why the code action is currently disabled.
 		 *
diff --git a/internal/lsp/rename.go b/internal/lsp/rename.go
index cef0638..5f27d23 100644
--- a/internal/lsp/rename.go
+++ b/internal/lsp/rename.go
@@ -43,9 +43,11 @@
 	}
 	// Do not return errors here, as it adds clutter.
 	// Returning a nil result means there is not a valid rename.
-	item, err := source.PrepareRename(ctx, snapshot, fh, params.Position)
+	item, usererr, err := source.PrepareRename(ctx, snapshot, fh, params.Position)
 	if err != nil {
-		return nil, nil // ignore errors
+		// Return usererr here rather than err, to avoid cluttering the UI with
+		// internal error details.
+		return nil, usererr
 	}
 	// TODO(suzmue): return ident.Name as the placeholder text.
 	return &item.Range, nil
diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go
index da164ed..79c0ab9 100755
--- a/internal/lsp/source/api_json.go
+++ b/internal/lsp/source/api_json.go
@@ -713,6 +713,11 @@
 			Doc:     "go_get_package runs `go get` to fetch a package.\n",
 		},
 		{
+			Command: "gopls.check_upgrades",
+			Title:   "Check for upgrades",
+			Doc:     "check_upgrades checks for module upgrades.\n",
+		},
+		{
 			Command: "gopls.add_dependency",
 			Title:   "Add dependency",
 			Doc:     "add_dependency adds a dependency.\n",
diff --git a/internal/lsp/source/command.go b/internal/lsp/source/command.go
index 16d57ff..f014e13 100644
--- a/internal/lsp/source/command.go
+++ b/internal/lsp/source/command.go
@@ -65,6 +65,7 @@
 	CommandUpdateGoSum,
 	CommandUndeclaredName,
 	CommandGoGetPackage,
+	CommandCheckUpgrades,
 	CommandAddDependency,
 	CommandUpgradeDependency,
 	CommandRemoveDependency,
@@ -113,6 +114,12 @@
 		Title: "Update go.sum",
 	}
 
+	// CommandCheckUpgrades checks for module upgrades.
+	CommandCheckUpgrades = &Command{
+		Name:  "check_upgrades",
+		Title: "Check for upgrades",
+	}
+
 	// CommandAddDependency adds a dependency.
 	CommandAddDependency = &Command{
 		Name:  "add_dependency",
diff --git a/internal/lsp/source/rename.go b/internal/lsp/source/rename.go
index fdf3f63..da7faf8 100644
--- a/internal/lsp/source/rename.go
+++ b/internal/lsp/source/rename.go
@@ -42,22 +42,30 @@
 	Text  string
 }
 
-func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) (*PrepareItem, error) {
+// PrepareRename searches for a valid renaming at position pp.
+//
+// The returned usererr is intended to be displayed to the user to explain why
+// the prepare fails. Probably we could eliminate the redundancy in returning
+// two errors, but for now this is done defensively.
+func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) (_ *PrepareItem, usererr, err error) {
 	ctx, done := event.Start(ctx, "source.PrepareRename")
 	defer done()
 
 	qos, err := qualifiedObjsAtProtocolPos(ctx, snapshot, f, pp)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 	node, obj, pkg := qos[0].node, qos[0].obj, qos[0].sourcePkg
+	if err := checkRenamable(obj); err != nil {
+		return nil, err, err
+	}
 	mr, err := posToMappedRange(snapshot, pkg, node.Pos(), node.End())
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 	rng, err := mr.Range()
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 	if _, isImport := node.(*ast.ImportSpec); isImport {
 		// We're not really renaming the import path.
@@ -66,10 +74,22 @@
 	return &PrepareItem{
 		Range: rng,
 		Text:  obj.Name(),
-	}, nil
+	}, nil, nil
 }
 
-// Rename returns a map of TextEdits for each file modified when renaming a given identifier within a package.
+// checkRenamable verifies if an obj may be renamed.
+func checkRenamable(obj types.Object) error {
+	if v, ok := obj.(*types.Var); ok && v.Embedded() {
+		return errors.New("can't rename embedded fields: rename the type directly or name the field")
+	}
+	if obj.Name() == "_" {
+		return errors.New("can't rename \"_\"")
+	}
+	return nil
+}
+
+// Rename returns a map of TextEdits for each file modified when renaming a
+// given identifier within a package.
 func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]protocol.TextEdit, error) {
 	ctx, done := event.Start(ctx, "source.Rename")
 	defer done()
@@ -79,9 +99,11 @@
 		return nil, err
 	}
 
-	obj := qos[0].obj
-	pkg := qos[0].pkg
+	obj, pkg := qos[0].obj, qos[0].pkg
 
+	if err := checkRenamable(obj); err != nil {
+		return nil, err
+	}
 	if obj.Name() == newName {
 		return nil, errors.Errorf("old and new names are the same: %s", newName)
 	}
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index bc670db..5ad2ebc 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -820,7 +820,7 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	item, err := source.PrepareRename(r.ctx, r.snapshot, fh, srcRng.Start)
+	item, _, err := source.PrepareRename(r.ctx, r.snapshot, fh, srcRng.Start)
 	if err != nil {
 		if want.Text != "" { // expected an ident.
 			t.Errorf("prepare rename failed for %v: got error: %v", src, err)
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 329c89a..7a576a6 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -109,10 +109,6 @@
 	// the given go.mod file.
 	ModWhy(ctx context.Context, fh FileHandle) (map[string]string, error)
 
-	// ModUpgrade returns the possible updates for the module specified by the
-	// given go.mod file.
-	ModUpgrade(ctx context.Context, fh FileHandle) (map[string]string, error)
-
 	// ModTidy returns the results of `go mod tidy` for the module specified by
 	// the given go.mod file.
 	ModTidy(ctx context.Context, pm *ParsedModule) (*TidiedModule, error)
@@ -231,6 +227,12 @@
 	// IsGoPrivatePath reports whether target is a private import path, as identified
 	// by the GOPRIVATE environment variable.
 	IsGoPrivatePath(path string) bool
+
+	// ModuleUpgrades returns known module upgrades.
+	ModuleUpgrades() map[string]string
+
+	// RegisterModuleUpgrades registers that upgrades exist for the given modules.
+	RegisterModuleUpgrades(upgrades map[string]string)
 }
 
 // A FileSource maps uris to FileHandles. This abstraction exists both for
@@ -596,6 +598,7 @@
 	TypeError
 	ModTidyError
 	Analysis
+	UpgradeNotification
 )
 
 func (e *Error) Error() string {
@@ -614,12 +617,22 @@
 // sure not to show this version to end users in error messages, to avoid
 // confusion.
 // The major version is not included, as that depends on the module path.
-const workspaceModuleVersion = ".0.0-goplsworkspace"
+//
+// If workspace module A is dependent on workspace module B, we need our
+// nonexistant version to be greater than the version A mentions.
+// Otherwise, the go command will try to update to that version. Use a very
+// high minor version to make that more likely.
+const workspaceModuleVersion = ".9999999.0-goplsworkspace"
 
 func IsWorkspaceModuleVersion(version string) bool {
 	return strings.HasSuffix(version, workspaceModuleVersion)
 }
 
 func WorkspaceModuleVersion(majorVersion string) string {
+	// Use the highest compatible major version to avoid unwanted upgrades.
+	// See the comment on workspaceModuleVersion.
+	if majorVersion == "v0" {
+		majorVersion = "v1"
+	}
 	return majorVersion + workspaceModuleVersion
 }
diff --git a/internal/lsp/testdata/good/good1.go b/internal/lsp/testdata/good/good1.go
index bdccaed..c4664a7 100644
--- a/internal/lsp/testdata/good/good1.go
+++ b/internal/lsp/testdata/good/good1.go
@@ -1,7 +1,6 @@
 package good //@diag("package", "no_diagnostics", "", "error")
 
 import (
-	_ "go/ast"                              //@prepare("go/ast", "_", "_")
 	"golang.org/x/tools/internal/lsp/types" //@item(types_import, "types", "\"golang.org/x/tools/internal/lsp/types\"", "package")
 )
 
diff --git a/internal/lsp/testdata/rename/issue43616/issue43616.go.golden b/internal/lsp/testdata/rename/issue43616/issue43616.go.golden
index 367d52d..34d03ba 100644
--- a/internal/lsp/testdata/rename/issue43616/issue43616.go.golden
+++ b/internal/lsp/testdata/rename/issue43616/issue43616.go.golden
@@ -1,9 +1,13 @@
 -- bar-rename --
 package issue43616
 
-type bar int //@rename("foo","bar")
+type bar int //@rename("foo","bar"),prepare("oo","foo","foo")
 
-var x struct{ bar }
+var x struct{ bar } //@rename("foo","baz")
 
-var _ = x.bar
+var _ = x.bar //@rename("foo","quux")
 
+-- baz-rename --
+can't rename embedded fields: rename the type directly or name the field
+-- quux-rename --
+can't rename embedded fields: rename the type directly or name the field
diff --git a/internal/lsp/testdata/rename/issue43616/issue43616.go.in b/internal/lsp/testdata/rename/issue43616/issue43616.go.in
index 3686914..aaad531 100644
--- a/internal/lsp/testdata/rename/issue43616/issue43616.go.in
+++ b/internal/lsp/testdata/rename/issue43616/issue43616.go.in
@@ -1,7 +1,7 @@
 package issue43616
 
-type foo int //@rename("foo","bar")
+type foo int //@rename("foo","bar"),prepare("oo","foo","foo")
 
-var x struct{ foo }
+var x struct{ foo } //@rename("foo","baz")
 
-var _ = x.foo
+var _ = x.foo //@rename("foo","quux")
diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden
index 5482a03..37bb48a 100644
--- a/internal/lsp/testdata/summary.txt.golden
+++ b/internal/lsp/testdata/summary.txt.golden
@@ -19,7 +19,7 @@
 TypeDefinitionsCount = 2
 HighlightsCount = 69
 ReferencesCount = 25
-RenamesCount = 31
+RenamesCount = 33
 PrepareRenamesCount = 7
 SymbolsCount = 5
 WorkspaceSymbolsCount = 20