all: merge master (9c97539) into gopls-release-branch.0.12

Also put back the x/tools replace directive in gopls/go.mod.

For golang/go#59818

Merge List:

+ 2023-05-24 9c97539a2 gopls/internal/lsp/cache: remove nested module warning
+ 2023-05-24 d44a094d8 gopls/internal/lsp/cmd: add a stats -anon flag to show anonymous data
+ 2023-05-24 e106694df gopls/internal/lsp: bundle certain quick-fixes with their diagnostic
+ 2023-05-24 5dc3f7433 gopls/internal/lsp/filecache: reenable memory cache layer
+ 2023-05-24 7e146a6c6 gopls/internal/lsp/cmd: simplify connection type
+ 2023-05-24 1e6066861 gopls/internal/regtest/workspace: unskip duplicate modules test
+ 2023-05-23 5ce721db5 gopls/doc: Fix broken links
+ 2023-05-23 7a03febee gopls/internal/lsp/cmd: remove vestiges of debugging golang/go#59475
+ 2023-05-22 a70f2bc21 gopls/internal/regtest/misc: update and unskip TestHoverIntLiteral
+ 2023-05-22 6997d196f gopls/internal/regtest/misc: unskip TestMajorOptionsChange
+ 2023-05-22 ec543c5a2 gopls/internal/lsp/cache: fix crash in Session.updateViewLocked
+ 2023-05-22 a12ee94f7 gopls/internal/regtest/misc: update some unilaterally skipped tests
+ 2023-05-22 5ff5cbb00 gopls: deprecate support for Go 1.16 and 1.17, update warnings
+ 2023-05-22 e6fd7f4c0 gopls/internal/lsp/cache: limit module scan to 100K files
+ 2023-05-22 9ca66ba88 gopls/internal/lsp/regtest: delete TestWatchReplaceTargets
+ 2023-05-22 edbfdbebf gopls/internal/lsp/source/completion: (unimported) add placeholders
+ 2023-05-22 3a5dbf351 gopls: add a new "subdirWatchPatterns" setting
+ 2023-05-22 3c0255176 internal/typesinternal: remove NewObjectpathFunc
+ 2023-05-20 07293620c present: reformat doc comment for lack of inline code
+ 2023-05-20 d4e66bd9a go/ssa: TestStdlib: disable check that function names are distinct
+ 2023-05-20 738ea2bdc go/ssa: use core type for field accesses
+ 2023-05-19 2ec4299f3 gopls/internal/lsp: split file-watching glob patterns
+ 2023-05-19 43b02eab0 gopls/internal/lsp/cache: only delete the most relevant mod tidy handle
+ 2023-05-19 5919673c9 internal/lsp/filecache: eliminate 'kind' directories
+ 2023-05-19 a5ef6c3eb gopls/internal/lsp: keep track of overlays on the files map
+ 2023-05-19 d7f4359f8 gopls/internal/lsp/mod: optimizations for mod tidy diagnostics
+ 2023-05-19 2eb726b88 gopls/internal/lsp/filecache: touch only files older than 1h
+ 2023-05-19 b742cb9a5 gopls/internal/regtest/bench: add a benchmark for diagnosing saves
+ 2023-05-19 4d66324ee gopls/internal/lsp/cache: tweak error message
+ 2023-05-19 e46df400e gopls/internal/lsp/filecache: delayed tweaks from code review
+ 2023-05-19 3df69b827 gopls/internal/lsp/debug: remove memory monitoring
+ 2023-05-19 a069704d0 gopls/internal/lsp/filecache: avoid flock
+ 2023-05-18 3d53c2d20 gopls/internal/lsp/cache: fix race in adhoc reloading
+ 2023-05-17 8b4b27bce go/analysis/passes/slog: fix Group kv offset
+ 2023-05-17 242e5ed73 cover: eliminate an unnecessary fsync in TestParseProfiles
+ 2023-05-17 651d951bb go/ssa: fix typo in package docs

Change-Id: Ie57d55ba8f3ae9bcf868db08088f68955b5e4856
diff --git a/cover/profile_test.go b/cover/profile_test.go
index 3cecdac..925b397 100644
--- a/cover/profile_test.go
+++ b/cover/profile_test.go
@@ -6,8 +6,8 @@
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
+	"path/filepath"
 	"reflect"
 	"testing"
 )
@@ -208,26 +208,12 @@
 
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
-			f, err := ioutil.TempFile("", "")
-			if err != nil {
-				t.Fatalf("Failed to create a temp file: %v.", err)
-			}
-			defer func() {
-				f.Close()
-				os.Remove(f.Name())
-			}()
-			n, err := f.WriteString(tc.input)
-			if err != nil {
-				t.Fatalf("Failed to write to temp file: %v", err)
-			}
-			if n < len(tc.input) {
-				t.Fatalf("Didn't write enough bytes to temp file (wrote %d, expected %d).", n, len(tc.input))
-			}
-			if err := f.Sync(); err != nil {
-				t.Fatalf("Failed to sync temp file: %v", err)
+			fname := filepath.Join(t.TempDir(), "test.cov")
+			if err := os.WriteFile(fname, []byte(tc.input), 0644); err != nil {
+				t.Fatal(err)
 			}
 
-			result, err := ParseProfiles(f.Name())
+			result, err := ParseProfiles(fname)
 			if err != nil {
 				if !tc.expectErr {
 					t.Errorf("Unexpected error: %v", err)
diff --git a/go/analysis/passes/slog/slog.go b/go/analysis/passes/slog/slog.go
index 874ebec..8429eab 100644
--- a/go/analysis/passes/slog/slog.go
+++ b/go/analysis/passes/slog/slog.go
@@ -204,7 +204,7 @@
 		"WarnCtx":  2,
 		"ErrorCtx": 2,
 		"Log":      3,
-		"Group":    0,
+		"Group":    1,
 	},
 	"Logger": map[string]int{
 		"Debug":    1,
diff --git a/go/analysis/passes/slog/testdata/src/a/a.go b/go/analysis/passes/slog/testdata/src/a/a.go
index a13aac7..aa408d0 100644
--- a/go/analysis/passes/slog/testdata/src/a/a.go
+++ b/go/analysis/passes/slog/testdata/src/a/a.go
@@ -143,7 +143,8 @@
 
 	r.Add(1, 2) // want `slog.Record.Add arg "1" should be a string or a slog.Attr`
 
-	_ = slog.Group("a", 1, 2, 3) // want `slog.Group arg "2" should be a string or a slog.Attr`
+	_ = slog.Group("key", "a", 1, "b", 2)
+	_ = slog.Group("key", "a", 1, 2, 3) // want `slog.Group arg "2" should be a string or a slog.Attr`
 
 }
 
diff --git a/go/ssa/builder_generic_test.go b/go/ssa/builder_generic_test.go
index 77de326..c86da0c 100644
--- a/go/ssa/builder_generic_test.go
+++ b/go/ssa/builder_generic_test.go
@@ -685,7 +685,7 @@
 	//@ instrs("f12", "*ssa.MakeMap", "make map[P]bool 1:int")
 	func f12[T any, P *struct{f T}](x T) map[P]bool { return map[P]bool{{}: true} }
 
-	//@ instrs("f13", "&v[0:int]")
+	//@ instrs("f13", "*ssa.IndexAddr", "&v[0:int]")
 	//@ instrs("f13", "*ssa.Store", "*t0 = 7:int", "*v = *new(A):A")
 	func f13[A [3]int, PA *A](v PA) {
 		*v = A{7}
diff --git a/go/ssa/doc.go b/go/ssa/doc.go
index afda476..a687de4 100644
--- a/go/ssa/doc.go
+++ b/go/ssa/doc.go
@@ -66,7 +66,6 @@
 //	*FieldAddr            ✔               ✔
 //	*FreeVar              ✔
 //	*Function             ✔                               ✔ (func)
-//	*GenericConvert       ✔               ✔
 //	*Global               ✔                               ✔ (var)
 //	*Go                                   ✔
 //	*If                                   ✔
@@ -80,6 +79,7 @@
 //	*MakeMap              ✔               ✔
 //	*MakeSlice            ✔               ✔
 //	*MapUpdate                            ✔
+//	*MultiConvert         ✔               ✔
 //	*NamedConst                                           ✔ (const)
 //	*Next                 ✔               ✔
 //	*Panic                                ✔
diff --git a/go/ssa/emit.go b/go/ssa/emit.go
index 80e30b6..fe2f6f0 100644
--- a/go/ssa/emit.go
+++ b/go/ssa/emit.go
@@ -476,7 +476,7 @@
 // value of a field.
 func emitImplicitSelections(f *Function, v Value, indices []int, pos token.Pos) Value {
 	for _, index := range indices {
-		if st, vptr := deptr(v.Type()); vptr {
+		if st, vptr := deref(v.Type()); vptr {
 			fld := fieldOf(st, index)
 			instr := &FieldAddr{
 				X:     v,
@@ -486,7 +486,7 @@
 			instr.setType(types.NewPointer(fld.Type()))
 			v = f.emit(instr)
 			// Load the field's value iff indirectly embedded.
-			if _, fldptr := deptr(fld.Type()); fldptr {
+			if _, fldptr := deref(fld.Type()); fldptr {
 				v = emitLoad(f, v)
 			}
 		} else {
@@ -510,7 +510,7 @@
 // field's value.
 // Ident id is used for position and debug info.
 func emitFieldSelection(f *Function, v Value, index int, wantAddr bool, id *ast.Ident) Value {
-	if st, vptr := deptr(v.Type()); vptr {
+	if st, vptr := deref(v.Type()); vptr {
 		fld := fieldOf(st, index)
 		instr := &FieldAddr{
 			X:     v,
diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go
index eeb9681..313146d 100644
--- a/go/ssa/ssa.go
+++ b/go/ssa/ssa.go
@@ -865,7 +865,7 @@
 type FieldAddr struct {
 	register
 	X     Value // *struct
-	Field int   // field is typeparams.CoreType(X.Type().Underlying().(*types.Pointer).Elem()).(*types.Struct).Field(Field)
+	Field int   // index into CoreType(CoreType(X.Type()).(*types.Pointer).Elem()).(*types.Struct).Fields
 }
 
 // The Field instruction yields the Field of struct X.
@@ -884,7 +884,7 @@
 type Field struct {
 	register
 	X     Value // struct
-	Field int   // index into typeparams.CoreType(X.Type()).(*types.Struct).Fields
+	Field int   // index into CoreType(X.Type()).(*types.Struct).Fields
 }
 
 // The IndexAddr instruction yields the address of the element at
diff --git a/go/ssa/stdlib_test.go b/go/ssa/stdlib_test.go
index 8b9f423..11782f7 100644
--- a/go/ssa/stdlib_test.go
+++ b/go/ssa/stdlib_test.go
@@ -36,7 +36,7 @@
 
 func TestStdlib(t *testing.T) {
 	if testing.Short() {
-		t.Skip("skipping in short mode; too slow (https://golang.org/issue/14113)")
+		t.Skip("skipping in short mode; too slow (https://golang.org/issue/14113)") // ~5s
 	}
 	testenv.NeedsTool(t, "go")
 
@@ -81,20 +81,33 @@
 
 	allFuncs := ssautil.AllFunctions(prog)
 
-	// Check that all non-synthetic functions have distinct names.
-	// Synthetic wrappers for exported methods should be distinct too,
-	// except for unexported ones (explained at (*Function).RelString).
-	byName := make(map[string]*ssa.Function)
-	for fn := range allFuncs {
-		if fn.Synthetic == "" || ast.IsExported(fn.Name()) {
-			str := fn.String()
-			prev := byName[str]
-			byName[str] = fn
-			if prev != nil {
-				t.Errorf("%s: duplicate function named %s",
-					prog.Fset.Position(fn.Pos()), str)
-				t.Errorf("%s:   (previously defined here)",
-					prog.Fset.Position(prev.Pos()))
+	// The assertion below is not valid if the program contains
+	// variants of the same package, such as the test variants
+	// (e.g. package p as compiled for test executable x) obtained
+	// when cfg.Tests=true. Profile-guided optimization may
+	// lead to similar variation for non-test executables.
+	//
+	// Ideally, the test would assert that all functions within
+	// each executable (more generally: within any singly rooted
+	// transitively closed subgraph of the import graph) have
+	// distinct names, but that isn't so easy to compute efficiently.
+	// Disabling for now.
+	if false {
+		// Check that all non-synthetic functions have distinct names.
+		// Synthetic wrappers for exported methods should be distinct too,
+		// except for unexported ones (explained at (*Function).RelString).
+		byName := make(map[string]*ssa.Function)
+		for fn := range allFuncs {
+			if fn.Synthetic == "" || ast.IsExported(fn.Name()) {
+				str := fn.String()
+				prev := byName[str]
+				byName[str] = fn
+				if prev != nil {
+					t.Errorf("%s: duplicate function named %s",
+						prog.Fset.Position(fn.Pos()), str)
+					t.Errorf("%s:   (previously defined here)",
+						prog.Fset.Position(prev.Pos()))
+				}
 			}
 		}
 	}
diff --git a/gopls/README.md b/gopls/README.md
index 56d1592..396f86c 100644
--- a/gopls/README.md
+++ b/gopls/README.md
@@ -93,6 +93,7 @@
 | ----------- | --------------------------------------------------- |
 | Go 1.12     | [gopls@v0.7.5](https://github.com/golang/tools/releases/tag/gopls%2Fv0.7.5) |
 | Go 1.15     | [gopls@v0.9.5](https://github.com/golang/tools/releases/tag/gopls%2Fv0.9.5) |
+| Go 1.17     | [gopls@v0.11.0](https://github.com/golang/tools/releases/tag/gopls%2Fv0.11.0) |
 
 Our extended support is enforced via [continuous integration with older Go
 versions](doc/contributing.md#ci). This legacy Go CI may not block releases:
diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md
index 8fe677b..b259f63 100644
--- a/gopls/doc/commands.md
+++ b/gopls/doc/commands.md
@@ -267,6 +267,9 @@
 	"URI": string,
 	// The module path to remove.
 	"ModulePath": string,
+	// If the module is tidied apart from the one unused diagnostic, we can
+	// run `go get module@none`, and then run `go mod tidy`. Otherwise, we
+	// must make textual edits.
 	"OnlyDiagnostic": bool,
 }
 ```
diff --git a/gopls/doc/design/implementation.md b/gopls/doc/design/implementation.md
index 859ec1c..e9b915b 100644
--- a/gopls/doc/design/implementation.md
+++ b/gopls/doc/design/implementation.md
@@ -37,12 +37,12 @@
 
 [gopls]: https://github.com/golang/tools/tree/master/gopls
 [internal/jsonrpc2]: https://github.com/golang/tools/tree/master/internal/jsonrpc2
-[internal/lsp]: https://github.com/golang/tools/tree/master/internal/lsp
-[internal/lsp/cache]: https://github.com/golang/tools/tree/master/internal/lsp/cache
-[internal/lsp/cmd]: https://github.com/golang/tools/tree/master/internal/lsp/cmd
-[internal/lsp/debug]: https://github.com/golang/tools/tree/master/internal/lsp/debug
-[internal/lsp/protocol]: https://github.com/golang/tools/tree/master/internal/lsp/protocol
-[internal/lsp/source]: https://github.com/golang/tools/tree/master/internal/lsp/source
+[internal/lsp]: https://github.com/golang/tools/tree/master/gopls/internal/lsp
+[internal/lsp/cache]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/cache
+[internal/lsp/cmd]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/cmd
+[internal/lsp/debug]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/debug
+[internal/lsp/protocol]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/protocol
+[internal/lsp/source]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/source
 [internal/memoize]: https://github.com/golang/tools/tree/master/internal/memoize
-[internal/span]: https://github.com/golang/tools/tree/master/internal/span
+[internal/span]: https://github.com/golang/tools/tree/master/gopls/internal/span
 [x/tools]: https://github.com/golang/tools
diff --git a/gopls/go.mod b/gopls/go.mod
index 5f64a62..ca3f9bf2 100644
--- a/gopls/go.mod
+++ b/gopls/go.mod
@@ -25,3 +25,5 @@
 	golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
 	golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 // indirect
 )
+
+replace golang.org/x/tools => ../
diff --git a/gopls/go.sum b/gopls/go.sum
index fe1d7bb..f6308a7 100644
--- a/gopls/go.sum
+++ b/gopls/go.sum
@@ -42,86 +42,42 @@
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
 golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y=
 golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
 golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
 golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
 golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
-golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
-golang.org/x/tools v0.4.1-0.20221217013628-b4dfc36097e2/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.9.2-0.20230516204147-76f78597112f h1:sbeNk1oZVYGPodwxlh++zvjWdh/C9nWD3lkPTLPkzDU=
-golang.org/x/tools v0.9.2-0.20230516204147-76f78597112f/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
 golang.org/x/vuln v0.0.0-20230110180137-6ad3e3d07815 h1:A9kONVi4+AnuOr1dopsibH6hLi1Huy54cbeJxnq4vmU=
 golang.org/x/vuln v0.0.0-20230110180137-6ad3e3d07815/go.mod h1:XJiVExZgoZfrrxoTeVsFYrSSk1snhfpOEC95JL+A4T0=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/gopls/internal/bug/bug.go b/gopls/internal/bug/bug.go
index 1bf7d30..f72948b 100644
--- a/gopls/internal/bug/bug.go
+++ b/gopls/internal/bug/bug.go
@@ -17,6 +17,7 @@
 	"runtime/debug"
 	"sort"
 	"sync"
+	"time"
 )
 
 // PanicOnBugs controls whether to panic when bugs are reported.
@@ -32,12 +33,15 @@
 
 // A Bug represents an unexpected event or broken invariant. They are used for
 // capturing metadata that helps us understand the event.
+//
+// Bugs are JSON-serializable.
 type Bug struct {
-	File        string // file containing the call to bug.Report
-	Line        int    // line containing the call to bug.Report
-	Description string // description of the bug
-	Key         string // key identifying the bug (file:line if available)
-	Stack       string // call stack
+	File        string    // file containing the call to bug.Report
+	Line        int       // line containing the call to bug.Report
+	Description string    // description of the bug
+	Key         string    // key identifying the bug (file:line if available)
+	Stack       string    // call stack
+	AtTime      time.Time // time the bug was reported
 }
 
 // Reportf reports a formatted bug message.
@@ -77,6 +81,7 @@
 		Description: description,
 		Key:         key,
 		Stack:       string(debug.Stack()),
+		AtTime:      time.Now(),
 	}
 
 	mu.Lock()
diff --git a/gopls/internal/bug/bug_test.go b/gopls/internal/bug/bug_test.go
index 2e36221..8ca2aa5 100644
--- a/gopls/internal/bug/bug_test.go
+++ b/gopls/internal/bug/bug_test.go
@@ -5,8 +5,12 @@
 package bug
 
 import (
+	"encoding/json"
 	"fmt"
 	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
 )
 
 func resetForTesting() {
@@ -62,3 +66,26 @@
 		t.Errorf("got %q, want %q", got, want)
 	}
 }
+
+func TestBugJSON(t *testing.T) {
+	b1 := Bug{
+		File:        "foo.go",
+		Line:        1,
+		Description: "a bug",
+		Key:         "foo.go:1",
+		Stack:       "<stack>",
+		AtTime:      time.Now(),
+	}
+
+	data, err := json.Marshal(b1)
+	if err != nil {
+		t.Fatal(err)
+	}
+	var b2 Bug
+	if err := json.Unmarshal(data, &b2); err != nil {
+		t.Fatal(err)
+	}
+	if diff := cmp.Diff(b1, b2); diff != "" {
+		t.Errorf("bugs differ after JSON Marshal/Unmarshal (-b1 +b2):\n%s", diff)
+	}
+}
diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go
index 83ea177..6631270 100644
--- a/gopls/internal/lsp/cache/check.go
+++ b/gopls/internal/lsp/cache/check.go
@@ -1190,7 +1190,7 @@
 
 		relatedInformation: s.view.Options().RelatedInformationSupported,
 		linkTarget:         s.view.Options().LinkTarget,
-		moduleMode:         s.moduleMode(),
+		moduleMode:         s.view.moduleMode(),
 	}, nil
 }
 
@@ -1542,14 +1542,18 @@
 				if err != nil {
 					return nil, err
 				}
-				errors = append(errors, &source.Diagnostic{
+				diag := &source.Diagnostic{
 					URI:            imp.cgf.URI,
 					Range:          rng,
 					Severity:       protocol.SeverityError,
 					Source:         source.TypeError,
 					Message:        fmt.Sprintf("error while importing %v: %v", item, depErr.Err),
 					SuggestedFixes: fixes,
-				})
+				}
+				if !source.BundleQuickFixes(diag) {
+					bug.Reportf("failed to bundle fixes for diagnostic %q", diag.Message)
+				}
+				errors = append(errors, diag)
 			}
 		}
 	}
@@ -1585,14 +1589,18 @@
 			if err != nil {
 				return nil, err
 			}
-			errors = append(errors, &source.Diagnostic{
+			diag := &source.Diagnostic{
 				URI:            pm.URI,
 				Range:          rng,
 				Severity:       protocol.SeverityError,
 				Source:         source.TypeError,
 				Message:        fmt.Sprintf("error while importing %v: %v", item, depErr.Err),
 				SuggestedFixes: fixes,
-			})
+			}
+			if !source.BundleQuickFixes(diag) {
+				bug.Reportf("failed to bundle fixes for diagnostic %q", diag.Message)
+			}
+			errors = append(errors, diag)
 			break
 		}
 	}
diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go
index 111b074..939d084 100644
--- a/gopls/internal/lsp/cache/load.go
+++ b/gopls/internal/lsp/cache/load.go
@@ -378,53 +378,6 @@
 		return fmt.Errorf(msg), s.applyCriticalErrorToFiles(ctx, msg, openFiles)
 	}
 
-	// If the user has one active go.mod file, they may still be editing files
-	// in nested modules. Check the module of each open file and add warnings
-	// that the nested module must be opened as a workspace folder.
-	if len(s.workspaceModFiles) == 1 {
-		// Get the active root go.mod file to compare against.
-		var rootMod string
-		for uri := range s.workspaceModFiles {
-			rootMod = uri.Filename()
-		}
-		rootDir := filepath.Dir(rootMod)
-		nestedModules := make(map[string][]*Overlay)
-		for _, fh := range openFiles {
-			mod, err := findRootPattern(ctx, filepath.Dir(fh.URI().Filename()), "go.mod", s)
-			if err != nil {
-				if ctx.Err() != nil {
-					return ctx.Err(), nil
-				}
-				continue
-			}
-			if mod == "" {
-				continue
-			}
-			if mod != rootMod && source.InDir(rootDir, mod) {
-				modDir := filepath.Dir(mod)
-				nestedModules[modDir] = append(nestedModules[modDir], fh)
-			}
-		}
-		var multiModuleMsg string
-		if s.view.goversion >= 18 {
-			multiModuleMsg = `To work on multiple modules at once, please use a go.work file.
-See https://github.com/golang/tools/blob/master/gopls/doc/workspace.md for more information on using workspaces.`
-		} else {
-			multiModuleMsg = `To work on multiple modules at once, please upgrade to Go 1.18 and use a go.work file.
-See https://github.com/golang/tools/blob/master/gopls/doc/workspace.md for more information on using workspaces.`
-		}
-		// Add a diagnostic to each file in a nested module to mark it as
-		// "orphaned". Don't show a general diagnostic in the progress bar,
-		// because the user may still want to edit a file in a nested module.
-		var srcDiags []*source.Diagnostic
-		for modDir, files := range nestedModules {
-			msg := fmt.Sprintf("This file is in %s, which is a nested module in the %s module.\n%s", modDir, rootMod, multiModuleMsg)
-			srcDiags = append(srcDiags, s.applyCriticalErrorToFiles(ctx, msg, files)...)
-		}
-		if len(srcDiags) != 0 {
-			return fmt.Errorf("You have opened a nested module.\n%s", multiModuleMsg), srcDiags
-		}
-	}
 	return nil, nil
 }
 
diff --git a/gopls/internal/lsp/cache/maps.go b/gopls/internal/lsp/cache/maps.go
index 0ad4ac9..533c339 100644
--- a/gopls/internal/lsp/cache/maps.go
+++ b/gopls/internal/lsp/cache/maps.go
@@ -15,7 +15,8 @@
 // TODO(euroelessar): Use generics once support for go1.17 is dropped.
 
 type filesMap struct {
-	impl *persistent.Map
+	impl       *persistent.Map
+	overlayMap map[span.URI]*Overlay // the subset that are overlays
 }
 
 // uriLessInterface is the < relation for "any" values containing span.URIs.
@@ -25,13 +26,19 @@
 
 func newFilesMap() filesMap {
 	return filesMap{
-		impl: persistent.NewMap(uriLessInterface),
+		impl:       persistent.NewMap(uriLessInterface),
+		overlayMap: make(map[span.URI]*Overlay),
 	}
 }
 
 func (m filesMap) Clone() filesMap {
+	overlays := make(map[span.URI]*Overlay, len(m.overlayMap))
+	for k, v := range m.overlayMap {
+		overlays[k] = v
+	}
 	return filesMap{
-		impl: m.impl.Clone(),
+		impl:       m.impl.Clone(),
+		overlayMap: overlays,
 	}
 }
 
@@ -55,10 +62,30 @@
 
 func (m filesMap) Set(key span.URI, value source.FileHandle) {
 	m.impl.Set(key, value, nil)
+
+	if o, ok := value.(*Overlay); ok {
+		m.overlayMap[key] = o
+	} else {
+		// Setting a non-overlay must delete the corresponding overlay, to preserve
+		// the accuracy of the overlay set.
+		delete(m.overlayMap, key)
+	}
 }
 
-func (m filesMap) Delete(key span.URI) {
+func (m *filesMap) Delete(key span.URI) {
 	m.impl.Delete(key)
+	delete(m.overlayMap, key)
+}
+
+// overlays returns a new unordered array of overlay files.
+func (m filesMap) overlays() []*Overlay {
+	// In practice we will always have at least one overlay, so there is no need
+	// to optimize for the len=0 case by returning a nil slice.
+	overlays := make([]*Overlay, 0, len(m.overlayMap))
+	for _, o := range m.overlayMap {
+		overlays = append(overlays, o)
+	}
+	return overlays
 }
 
 func packageIDLessInterface(x, y interface{}) bool {
diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go
index b5e2dea..8dd555d 100644
--- a/gopls/internal/lsp/cache/mod_tidy.go
+++ b/gopls/internal/lsp/cache/mod_tidy.go
@@ -183,7 +183,33 @@
 	// go.mod file. The fixes will be for the go.mod file, but the
 	// diagnostics should also appear in both the go.mod file and the import
 	// statements in the Go files in which the dependencies are used.
+	// Finally, add errors for any unused dependencies.
+	if len(missing) > 0 {
+		missingModuleDiagnostics, err := missingModuleDiagnostics(ctx, snapshot, pm, ideal, missing)
+		if err != nil {
+			return nil, err
+		}
+		diagnostics = append(diagnostics, missingModuleDiagnostics...)
+	}
+
+	// Opt: if this is the only diagnostic, we can avoid textual edits and just
+	// run the Go command.
+	//
+	// See also the documentation for command.RemoveDependencyArgs.OnlyDiagnostic.
+	onlyDiagnostic := len(diagnostics) == 0 && len(unused) == 1
+	for _, req := range unused {
+		srcErr, err := unusedDiagnostic(pm.Mapper, req, onlyDiagnostic)
+		if err != nil {
+			return nil, err
+		}
+		diagnostics = append(diagnostics, srcErr)
+	}
+	return diagnostics, nil
+}
+
+func missingModuleDiagnostics(ctx context.Context, snapshot *snapshot, pm *source.ParsedModule, ideal *modfile.File, missing map[string]*modfile.Require) ([]*source.Diagnostic, error) {
 	missingModuleFixes := map[*modfile.Require][]source.SuggestedFix{}
+	var diagnostics []*source.Diagnostic
 	for _, req := range missing {
 		srcDiag, err := missingModuleDiagnostic(pm, req)
 		if err != nil {
@@ -290,15 +316,6 @@
 			}
 		}
 	}
-	// Finally, add errors for any unused dependencies.
-	onlyDiagnostic := len(diagnostics) == 0 && len(unused) == 1
-	for _, req := range unused {
-		srcErr, err := unusedDiagnostic(pm.Mapper, req, onlyDiagnostic)
-		if err != nil {
-			return nil, err
-		}
-		diagnostics = append(diagnostics, srcErr)
-	}
 	return diagnostics, nil
 }
 
diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go
index eaad67c..8eae64e 100644
--- a/gopls/internal/lsp/cache/session.go
+++ b/gopls/internal/lsp/cache/session.go
@@ -109,13 +109,14 @@
 
 // TODO(rfindley): clarify that createView can never be cancelled (with the
 // possible exception of server shutdown).
+// On success, the caller becomes responsible for calling the release function once.
 func (s *Session) createView(ctx context.Context, name string, folder span.URI, options *source.Options, seqID uint64) (*View, *snapshot, func(), error) {
 	index := atomic.AddInt64(&viewIndex, 1)
 
 	// Get immutable workspace information.
 	info, err := s.getWorkspaceInformation(ctx, folder, options)
 	if err != nil {
-		return nil, nil, func() {}, err
+		return nil, nil, nil, err
 	}
 
 	gowork, _ := info.GOWORK()
@@ -327,6 +328,15 @@
 	}
 
 	v, snapshot, release, err := s.createView(ctx, view.name, view.folder, options, seqID)
+	if err != nil {
+		// we have dropped the old view, but could not create the new one
+		// this should not happen and is very bad, but we still need to clean
+		// up the view array if it happens
+		s.views = removeElement(s.views, i)
+		return nil, err
+	}
+	defer release()
+
 	// The new snapshot has lost the history of the previous view. As a result,
 	// it may not see open files that aren't in its build configuration (as it
 	// would have done via didOpen notifications). This can lead to inconsistent
@@ -336,15 +346,7 @@
 	for _, o := range v.fs.Overlays() {
 		_, _ = snapshot.ReadFile(ctx, o.URI())
 	}
-	release()
 
-	if err != nil {
-		// we have dropped the old view, but could not create the new one
-		// this should not happen and is very bad, but we still need to clean
-		// up the view array if it happens
-		s.views = removeElement(s.views, i)
-		return nil, err
-	}
 	// substitute the new view into the array where the old view was
 	s.views[i] = v
 	return v, nil
@@ -596,20 +598,17 @@
 	for _, c := range changes {
 		if !knownDirs.Contains(c.URI) {
 			result = append(result, c)
-			continue
+		} else {
+			for uri := range knownFilesInDir(ctx, snapshots, c.URI) {
+				result = append(result, source.FileModification{
+					URI:        uri,
+					Action:     c.Action,
+					LanguageID: "",
+					OnDisk:     c.OnDisk,
+					// changes to directories cannot include text or versions
+				})
+			}
 		}
-		affectedFiles := knownFilesInDir(ctx, snapshots, c.URI)
-		var fileChanges []source.FileModification
-		for uri := range affectedFiles {
-			fileChanges = append(fileChanges, source.FileModification{
-				URI:        uri,
-				Action:     c.Action,
-				LanguageID: "",
-				OnDisk:     c.OnDisk,
-				// changes to directories cannot include text or versions
-			})
-		}
-		result = append(result, fileChanges...)
 	}
 	return result
 }
@@ -738,9 +737,10 @@
 	return nil
 }
 
-// FileWatchingGlobPatterns returns glob patterns to watch every directory
-// known by the view. For views within a module, this is the module root,
-// any directory in the module root, and any replace targets.
+// FileWatchingGlobPatterns returns a new set of glob patterns to
+// watch every directory known by the view. For views within a module,
+// this is the module root, any directory in the module root, and any
+// replace targets.
 func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[string]struct{} {
 	s.viewMu.Lock()
 	defer s.viewMu.Unlock()
diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go
index 7988a72..de9524b 100644
--- a/gopls/internal/lsp/cache/snapshot.go
+++ b/gopls/internal/lsp/cache/snapshot.go
@@ -159,10 +159,10 @@
 	modWhyHandles  *persistent.Map // from span.URI to *memoize.Promise[modWhyResult]
 	modVulnHandles *persistent.Map // from span.URI to *memoize.Promise[modVulnResult]
 
-	// knownSubdirs is the set of subdirectories in the workspace, used to
-	// create glob patterns for file watching.
-	knownSubdirs             knownDirsSet
-	knownSubdirsPatternCache string
+	// knownSubdirs is the set of subdirectory URIs in the workspace,
+	// used to create glob patterns for file watching.
+	knownSubdirs      knownDirsSet
+	knownSubdirsCache map[string]struct{} // memo of knownSubdirs as a set of filenames
 	// unprocessedSubdirChanges are any changes that might affect the set of
 	// subdirectories in the workspace. They are not reflected to knownSubdirs
 	// during the snapshot cloning step as it can slow down cloning.
@@ -327,47 +327,19 @@
 	if s.view.hasGopackagesDriver {
 		return true
 	}
+
 	// Check if the user is working within a module or if we have found
 	// multiple modules in the workspace.
 	if len(s.workspaceModFiles) > 0 {
 		return true
 	}
-	// The user may have a multiple directories in their GOPATH.
-	// Check if the workspace is within any of them.
+
 	// TODO(rfindley): this should probably be subject to "if GO111MODULES = off {...}".
-	for _, gp := range filepath.SplitList(s.view.gopath) {
-		if source.InDir(filepath.Join(gp, "src"), s.view.folder.Filename()) {
-			return true
-		}
-	}
-	return false
-}
-
-// moduleMode reports whether the current snapshot uses Go modules.
-//
-// From https://go.dev/ref/mod, module mode is active if either of the
-// following hold:
-//   - GO111MODULE=on
-//   - GO111MODULE=auto and we are inside a module or have a GOWORK value.
-//
-// Additionally, this method returns false if GOPACKAGESDRIVER is set.
-//
-// TODO(rfindley): use this more widely.
-func (s *snapshot) moduleMode() bool {
-	// Since we only really understand the `go` command, if the user has a
-	// different GOPACKAGESDRIVER, assume that their configuration is valid.
-	if s.view.hasGopackagesDriver {
-		return false
-	}
-
-	switch s.view.effectiveGO111MODULE() {
-	case on:
+	if s.view.inGOPATH {
 		return true
-	case off:
-		return false
-	default:
-		return len(s.workspaceModFiles) > 0 || s.view.gowork != ""
 	}
+
+	return false
 }
 
 // workspaceMode describes the way in which the snapshot's workspace should
@@ -652,21 +624,11 @@
 	return overlays
 }
 
-// TODO(rfindley): investigate whether it would be worthwhile to keep track of
-// overlays when we get them via GetFile.
 func (s *snapshot) overlays() []*Overlay {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
-	var overlays []*Overlay
-	s.files.Range(func(uri span.URI, fh source.FileHandle) {
-		overlay, ok := fh.(*Overlay)
-		if !ok {
-			return
-		}
-		overlays = append(overlays, overlay)
-	})
-	return overlays
+	return s.files.overlays()
 }
 
 // Package data kinds, identifying various package data that may be stored in
@@ -763,6 +725,18 @@
 }
 
 func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]*source.Metadata, error) {
+	if s.view.ViewType() == AdHocView {
+		// As described in golang/go#57209, in ad-hoc workspaces (where we load ./
+		// rather than ./...), preempting the directory load with file loads can
+		// lead to an inconsistent outcome, where certain files are loaded with
+		// command-line-arguments packages and others are loaded only in the ad-hoc
+		// package. Therefore, ensure that the workspace is loaded before doing any
+		// file loads.
+		if err := s.awaitLoaded(ctx); err != nil {
+			return nil, err
+		}
+	}
+
 	s.mu.Lock()
 
 	// Start with the set of package associations derived from the last load.
@@ -962,19 +936,57 @@
 		patterns[fmt.Sprintf("%s/**/*.{%s}", dirName, extensions)] = struct{}{}
 	}
 
-	// Some clients do not send notifications for changes to directories that
-	// contain Go code (golang/go#42348). To handle this, explicitly watch all
-	// of the directories in the workspace. We find them by adding the
-	// directories of every file in the snapshot's workspace directories.
-	// There may be thousands.
-	if pattern := s.getKnownSubdirsPattern(dirs); pattern != "" {
-		patterns[pattern] = struct{}{}
+	if s.watchSubdirs() {
+		// Some clients (e.g. VS Code) do not send notifications for changes to
+		// directories that contain Go code (golang/go#42348). To handle this,
+		// explicitly watch all of the directories in the workspace. We find them
+		// by adding the directories of every file in the snapshot's workspace
+		// directories. There may be thousands of patterns, each a single
+		// directory.
+		//
+		// (A previous iteration created a single glob pattern holding a union of
+		// all the directories, but this was found to cause VS Code to get stuck
+		// for several minutes after a buffer was saved twice in a workspace that
+		// had >8000 watched directories.)
+		//
+		// Some clients (notably coc.nvim, which uses watchman for globs) perform
+		// poorly with a large list of individual directories.
+		s.addKnownSubdirs(patterns, dirs)
 	}
 
 	return patterns
 }
 
-func (s *snapshot) getKnownSubdirsPattern(wsDirs []span.URI) string {
+// watchSubdirs reports whether gopls should request separate file watchers for
+// each relevant subdirectory. This is necessary only for clients (namely VS
+// Code) that do not send notifications for individual files in a directory
+// when the entire directory is deleted.
+func (s *snapshot) watchSubdirs() bool {
+	opts := s.view.Options()
+	switch p := opts.SubdirWatchPatterns; p {
+	case source.SubdirWatchPatternsOn:
+		return true
+	case source.SubdirWatchPatternsOff:
+		return false
+	case source.SubdirWatchPatternsAuto:
+		// See the documentation of InternalOptions.SubdirWatchPatterns for an
+		// explanation of why VS Code gets a different default value here.
+		//
+		// Unfortunately, there is no authoritative list of client names, nor any
+		// requirements that client names do not change. We should update the VS
+		// Code extension to set a default value of "subdirWatchPatterns" to "on",
+		// so that this workaround is only temporary.
+		if opts.ClientInfo != nil && opts.ClientInfo.Name == "Visual Studio Code" {
+			return true
+		}
+		return false
+	default:
+		bug.Reportf("invalid subdirWatchPatterns: %q", p)
+		return false
+	}
+}
+
+func (s *snapshot) addKnownSubdirs(patterns map[string]struct{}, wsDirs []span.URI) {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
@@ -983,23 +995,18 @@
 	// It may change list of known subdirs and therefore invalidate the cache.
 	s.applyKnownSubdirsChangesLocked(wsDirs)
 
-	if s.knownSubdirsPatternCache == "" {
-		var builder strings.Builder
+	// TODO(adonovan): is it still necessary to memoize the Range
+	// and URI.Filename operations?
+	if s.knownSubdirsCache == nil {
+		s.knownSubdirsCache = make(map[string]struct{})
 		s.knownSubdirs.Range(func(uri span.URI) {
-			if builder.Len() == 0 {
-				builder.WriteString("{")
-			} else {
-				builder.WriteString(",")
-			}
-			builder.WriteString(uri.Filename())
+			s.knownSubdirsCache[uri.Filename()] = struct{}{}
 		})
-		if builder.Len() > 0 {
-			builder.WriteString("}")
-			s.knownSubdirsPatternCache = builder.String()
-		}
 	}
 
-	return s.knownSubdirsPatternCache
+	for pattern := range s.knownSubdirsCache {
+		patterns[pattern] = struct{}{}
+	}
 }
 
 // collectAllKnownSubdirs collects all of the subdirectories within the
@@ -1013,7 +1020,7 @@
 
 	s.knownSubdirs.Destroy()
 	s.knownSubdirs = newKnownDirsSet()
-	s.knownSubdirsPatternCache = ""
+	s.knownSubdirsCache = nil
 	s.files.Range(func(uri span.URI, fh source.FileHandle) {
 		s.addKnownSubdirLocked(uri, dirs)
 	})
@@ -1072,7 +1079,7 @@
 		}
 		s.knownSubdirs.Insert(uri)
 		dir = filepath.Dir(dir)
-		s.knownSubdirsPatternCache = ""
+		s.knownSubdirsCache = nil
 	}
 }
 
@@ -1085,7 +1092,7 @@
 		}
 		if info, _ := os.Stat(dir); info == nil {
 			s.knownSubdirs.Remove(uri)
-			s.knownSubdirsPatternCache = ""
+			s.knownSubdirsCache = nil
 		}
 		dir = filepath.Dir(dir)
 	}
@@ -1849,7 +1856,7 @@
 					fix = `To work with multiple modules simultaneously, please upgrade to Go 1.18 or
 later, reinstall gopls, and use a go.work file.`
 				}
-				msg = fmt.Sprintf(`This file is in directory %q, which is not included in your workspace.
+				msg = fmt.Sprintf(`This file is within module %q, which is not included in your workspace.
 %s
 See the documentation for more information on setting up your workspace:
 https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`, modDir, fix)
@@ -2050,7 +2057,7 @@
 	// changed files. We need to rebuild the workspace module to know the
 	// true set of known subdirectories, but we don't want to do that in clone.
 	result.knownSubdirs = s.knownSubdirs.Clone()
-	result.knownSubdirsPatternCache = s.knownSubdirsPatternCache
+	result.knownSubdirsCache = s.knownSubdirsCache
 	for _, c := range changes {
 		result.unprocessedSubdirChanges = append(result.unprocessedSubdirChanges, c)
 	}
@@ -2109,10 +2116,36 @@
 		// Invalidate the previous modTidyHandle if any of the files have been
 		// saved or if any of the metadata has been invalidated.
 		if invalidateMetadata || fileWasSaved(originalFH, change.fileHandle) {
-			// TODO(maybe): Only delete mod handles for
-			// which the withoutURI is relevant.
-			// Requires reverse-engineering the go command. (!)
-			result.modTidyHandles.Clear()
+			// Only invalidate mod tidy results for the most relevant modfile in the
+			// workspace. This is a potentially lossy optimization for workspaces
+			// with many modules (such as google-cloud-go, which has 145 modules as
+			// of writing).
+			//
+			// While it is theoretically possible that a change in workspace module A
+			// could affect the mod-tidiness of workspace module B (if B transitively
+			// requires A), such changes are probably unlikely and not worth the
+			// penalty of re-running go mod tidy for everything. Note that mod tidy
+			// ignores GOWORK, so the two modules would have to be related by a chain
+			// of replace directives.
+			//
+			// We could improve accuracy by inspecting replace directives, using
+			// overlays in go mod tidy, and/or checking for metadata changes from the
+			// on-disk content.
+			//
+			// Note that we iterate the modTidyHandles map here, rather than e.g.
+			// using nearestModFile, because we don't have access to an accurate
+			// FileSource at this point in the snapshot clone.
+			const onlyInvalidateMostRelevant = true
+			if onlyInvalidateMostRelevant {
+				deleteMostRelevantModFile(result.modTidyHandles, uri)
+			} else {
+				result.modTidyHandles.Clear()
+			}
+
+			// TODO(rfindley): should we apply the above heuristic to mod vuln
+			// or mod handles as well?
+			//
+			// TODO(rfindley): no tests fail if I delete the below line.
 			result.modWhyHandles.Clear()
 			result.modVulnHandles.Clear()
 		}
@@ -2303,6 +2336,31 @@
 	return result, release
 }
 
+// deleteMostRelevantModFile deletes the mod file most likely to be the mod
+// file for the changed URI, if it exists.
+//
+// Specifically, this is the longest mod file path in a directory containing
+// changed. This might not be accurate if there is another mod file closer to
+// changed that happens not to be present in the map, but that's OK: the goal
+// of this function is to guarantee that IF the nearest mod file is present in
+// the map, it is invalidated.
+func deleteMostRelevantModFile(m *persistent.Map, changed span.URI) {
+	var mostRelevant span.URI
+	changedFile := changed.Filename()
+
+	m.Range(func(key, value interface{}) {
+		modURI := key.(span.URI)
+		if len(modURI) > len(mostRelevant) {
+			if source.InDir(filepath.Dir(modURI.Filename()), changedFile) {
+				mostRelevant = modURI
+			}
+		}
+	})
+	if mostRelevant != "" {
+		m.Delete(mostRelevant)
+	}
+}
+
 // invalidatedPackageIDs returns all packages invalidated by a change to uri.
 // If we haven't seen this URI before, we guess based on files in the same
 // directory. This is of course incorrect in build systems where packages are
diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go
index d999170..1dc13aa 100644
--- a/gopls/internal/lsp/cache/view.go
+++ b/gopls/internal/lsp/cache/view.go
@@ -130,6 +130,10 @@
 	// GOPACKAGESDRIVER environment variable or a gopackagesdriver binary on
 	// their machine.
 	hasGopackagesDriver bool
+
+	// inGOPATH reports whether the workspace directory is contained in a GOPATH
+	// directory.
+	inGOPATH bool
 }
 
 // effectiveGO111MODULE reports the value of GO111MODULE effective in the go
@@ -145,6 +149,79 @@
 	}
 }
 
+// A ViewType describes how we load package information for a view.
+//
+// This is used for constructing the go/packages.Load query, and for
+// interpreting missing packages, imports, or errors.
+//
+// Each view has a ViewType which is derived from its immutable workspace
+// information -- any environment change that would affect the view type
+// results in a new view.
+type ViewType int
+
+const (
+	// GoPackagesDriverView is a view with a non-empty GOPACKAGESDRIVER
+	// environment variable.
+	GoPackagesDriverView ViewType = iota
+
+	// GOPATHView is a view in GOPATH mode.
+	//
+	// I.e. in GOPATH, with GO111MODULE=off, or GO111MODULE=auto with no
+	// go.mod file.
+	GOPATHView
+
+	// GoModuleView is a view in module mode with a single Go module.
+	GoModuleView
+
+	// GoWorkView is a view in module mode with a go.work file.
+	GoWorkView
+
+	// An AdHocView is a collection of files in a given directory, not in GOPATH
+	// or a module.
+	AdHocView
+)
+
+// ViewType derives the type of the view from its workspace information.
+//
+// TODO(rfindley): this logic is overlapping and slightly inconsistent with
+// validBuildConfiguration. As part of zero-config-gopls (golang/go#57979), fix
+// this inconsistency and consolidate on the ViewType abstraction.
+func (w workspaceInformation) ViewType() ViewType {
+	if w.hasGopackagesDriver {
+		return GoPackagesDriverView
+	}
+	go111module := w.effectiveGO111MODULE()
+	if w.gowork != "" && go111module != off {
+		return GoWorkView
+	}
+	if w.gomod != "" && go111module != off {
+		return GoModuleView
+	}
+	if w.inGOPATH && go111module != on {
+		return GOPATHView
+	}
+	return AdHocView
+}
+
+// moduleMode reports whether the current snapshot uses Go modules.
+//
+// From https://go.dev/ref/mod, module mode is active if either of the
+// following hold:
+//   - GO111MODULE=on
+//   - GO111MODULE=auto and we are inside a module or have a GOWORK value.
+//
+// Additionally, this method returns false if GOPACKAGESDRIVER is set.
+//
+// TODO(rfindley): use this more widely.
+func (w workspaceInformation) moduleMode() bool {
+	switch w.ViewType() {
+	case GoModuleView, GoWorkView:
+		return true
+	default:
+		return false
+	}
+}
+
 // GOWORK returns the effective GOWORK value for this workspace, if
 // any, in URI form.
 //
@@ -740,6 +817,8 @@
 		})
 	}
 
+	// TODO(rfindley): this should be predicated on the s.view.moduleMode().
+	// There is no point loading ./... if we have an empty go.work.
 	if len(s.workspaceModFiles) > 0 {
 		for modURI := range s.workspaceModFiles {
 			// Verify that the modfile is valid before trying to load it.
@@ -881,7 +960,7 @@
 	if err != nil {
 		return info, err
 	}
-	if err := info.goEnv.load(ctx, folder.Filename(), options.EnvSlice(), s.gocmdRunner); err != nil {
+	if err := info.load(ctx, folder.Filename(), options.EnvSlice(), s.gocmdRunner); err != nil {
 		return info, err
 	}
 	// The value of GOPACKAGESDRIVER is not returned through the go command.
@@ -899,6 +978,13 @@
 		return info, err
 	}
 
+	// Check if the workspace is within any GOPATH directory.
+	for _, gp := range filepath.SplitList(info.gopath) {
+		if source.InDir(filepath.Join(gp, "src"), folder.Filename()) {
+			info.inGOPATH = true
+			break
+		}
+	}
 	return info, nil
 }
 
diff --git a/gopls/internal/lsp/cache/workspace.go b/gopls/internal/lsp/cache/workspace.go
index de36da6..28179f5 100644
--- a/gopls/internal/lsp/cache/workspace.go
+++ b/gopls/internal/lsp/cache/workspace.go
@@ -8,6 +8,7 @@
 	"context"
 	"errors"
 	"fmt"
+	"io/fs"
 	"os"
 	"path/filepath"
 	"sort"
@@ -127,7 +128,10 @@
 
 // Limit go.mod search to 1 million files. As a point of reference,
 // Kubernetes has 22K files (as of 2020-11-24).
-const fileLimit = 1000000
+//
+// Note: per golang/go#56496, the previous limit of 1M files was too slow, at
+// which point this limit was decreased to 100K.
+const fileLimit = 100_000
 
 // findModules recursively walks the root directory looking for go.mod files,
 // returning the set of modules it discovers. If modLimit is non-zero,
@@ -139,7 +143,7 @@
 	modFiles := make(map[span.URI]struct{})
 	searched := 0
 	errDone := errors.New("done")
-	err := filepath.Walk(root.Filename(), func(path string, info os.FileInfo, err error) error {
+	err := filepath.WalkDir(root.Filename(), func(path string, info fs.DirEntry, err error) error {
 		if err != nil {
 			// Probably a permission error. Keep looking.
 			return filepath.SkipDir
diff --git a/gopls/internal/lsp/cmd/capabilities_test.go b/gopls/internal/lsp/cmd/capabilities_test.go
index 39b60af..6d4e32f 100644
--- a/gopls/internal/lsp/cmd/capabilities_test.go
+++ b/gopls/internal/lsp/cmd/capabilities_test.go
@@ -41,17 +41,16 @@
 	defer os.RemoveAll(tmpDir)
 
 	app := New("gopls-test", tmpDir, os.Environ(), nil)
-	c := newConnection(app, nil)
-	ctx := context.Background()
-	defer c.terminate(ctx)
 
 	params := &protocol.ParamInitialize{}
-	params.RootURI = protocol.URIFromPath(c.Client.app.wd)
+	params.RootURI = protocol.URIFromPath(app.wd)
 	params.Capabilities.Workspace.Configuration = true
 
 	// Send an initialize request to the server.
-	c.Server = lsp.NewServer(cache.NewSession(ctx, cache.New(nil), app.options), c.Client)
-	result, err := c.Server.Initialize(ctx, params)
+	ctx := context.Background()
+	client := newClient(app, nil)
+	server := lsp.NewServer(cache.NewSession(ctx, cache.New(nil), app.options), client)
+	result, err := server.Initialize(ctx, params)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -60,10 +59,13 @@
 		t.Error(err)
 	}
 	// Complete initialization of server.
-	if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
+	if err := server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
 		t.Fatal(err)
 	}
 
+	c := newConnection(server, client)
+	defer c.terminate(ctx)
+
 	// Open the file on the server side.
 	uri := protocol.URIFromPath(tmpFile)
 	if err := c.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
diff --git a/gopls/internal/lsp/cmd/check.go b/gopls/internal/lsp/cmd/check.go
index f501c44..a529f14 100644
--- a/gopls/internal/lsp/cmd/check.go
+++ b/gopls/internal/lsp/cmd/check.go
@@ -57,8 +57,8 @@
 	if err := conn.diagnoseFiles(ctx, uris); err != nil {
 		return err
 	}
-	conn.Client.filesMu.Lock()
-	defer conn.Client.filesMu.Unlock()
+	conn.client.filesMu.Lock()
+	defer conn.client.filesMu.Unlock()
 
 	for _, file := range checking {
 		for _, d := range file.diagnostics {
diff --git a/gopls/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go
index 02e135a..0bd636c 100644
--- a/gopls/internal/lsp/cmd/cmd.go
+++ b/gopls/internal/lsp/cmd/cmd.go
@@ -293,10 +293,14 @@
 func (app *Application) connect(ctx context.Context, onProgress func(*protocol.ProgressParams)) (*connection, error) {
 	switch {
 	case app.Remote == "":
-		connection := newConnection(app, onProgress)
-		connection.Server = lsp.NewServer(cache.NewSession(ctx, cache.New(nil), app.options), connection.Client)
-		ctx = protocol.WithClient(ctx, connection.Client)
-		return connection, connection.initialize(ctx, app.options)
+		client := newClient(app, onProgress)
+		server := lsp.NewServer(cache.NewSession(ctx, cache.New(nil), app.options), client)
+		conn := newConnection(server, client)
+		if err := conn.initialize(protocol.WithClient(ctx, client), app.options); err != nil {
+			return nil, err
+		}
+		return conn, nil
+
 	case strings.HasPrefix(app.Remote, "internal@"):
 		internalMu.Lock()
 		defer internalMu.Unlock()
@@ -331,19 +335,19 @@
 }
 
 func (app *Application) connectRemote(ctx context.Context, remote string) (*connection, error) {
-	connection := newConnection(app, nil)
 	conn, err := lsprpc.ConnectToRemote(ctx, remote)
 	if err != nil {
 		return nil, err
 	}
 	stream := jsonrpc2.NewHeaderStream(conn)
 	cc := jsonrpc2.NewConn(stream)
-	connection.Server = protocol.ServerDispatcher(cc)
-	ctx = protocol.WithClient(ctx, connection.Client)
+	server := protocol.ServerDispatcher(cc)
+	client := newClient(app, nil)
+	connection := newConnection(server, client)
+	ctx = protocol.WithClient(ctx, connection.client)
 	cc.Go(ctx,
 		protocol.Handlers(
-			protocol.ClientHandler(connection.Client,
-				jsonrpc2.MethodNotFound)))
+			protocol.ClientHandler(client, jsonrpc2.MethodNotFound)))
 	return connection, connection.initialize(ctx, app.options)
 }
 
@@ -355,7 +359,7 @@
 
 func (c *connection) initialize(ctx context.Context, options func(*source.Options)) error {
 	params := &protocol.ParamInitialize{}
-	params.RootURI = protocol.URIFromPath(c.Client.app.wd)
+	params.RootURI = protocol.URIFromPath(c.client.app.wd)
 	params.Capabilities.Workspace.Configuration = true
 
 	// Make sure to respect configured options when sending initialize request.
@@ -377,7 +381,7 @@
 
 	// If the subcommand has registered a progress handler, report the progress
 	// capability.
-	if c.Client.onProgress != nil {
+	if c.client.onProgress != nil {
 		params.Capabilities.Window.WorkDoneProgress = true
 	}
 
@@ -395,11 +399,10 @@
 
 type connection struct {
 	protocol.Server
-	Client *cmdClient
+	client *cmdClient
 }
 
 type cmdClient struct {
-	protocol.Server
 	app        *Application
 	onProgress func(*protocol.ProgressParams)
 
@@ -417,13 +420,18 @@
 	diagnostics []protocol.Diagnostic
 }
 
-func newConnection(app *Application, onProgress func(*protocol.ProgressParams)) *connection {
+func newClient(app *Application, onProgress func(*protocol.ProgressParams)) *cmdClient {
+	return &cmdClient{
+		app:        app,
+		onProgress: onProgress,
+		files:      make(map[span.URI]*cmdFile),
+	}
+}
+
+func newConnection(server protocol.Server, client *cmdClient) *connection {
 	return &connection{
-		Client: &cmdClient{
-			app:        app,
-			onProgress: onProgress,
-			files:      make(map[span.URI]*cmdFile),
-		},
+		Server: server,
+		client: client,
 	}
 }
 
@@ -518,11 +526,6 @@
 }
 
 func (c *cmdClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishDiagnosticsParams) error {
-	var debug = os.Getenv(DebugSuggestedFixEnvVar) == "true"
-	if debug {
-		log.Printf("PublishDiagnostics URI=%v Diagnostics=%v", p.URI, p.Diagnostics)
-	}
-
 	if p.URI == "gopls://diagnostics-done" {
 		close(c.diagnosticsDone)
 	}
@@ -616,7 +619,7 @@
 // - map a (URI, protocol.Range) to a MappedRange;
 // - parse a command-line argument to a MappedRange.
 func (c *connection) openFile(ctx context.Context, uri span.URI) (*cmdFile, error) {
-	file := c.Client.openFile(ctx, uri)
+	file := c.client.openFile(ctx, uri)
 	if file.err != nil {
 		return nil, file.err
 	}
@@ -651,22 +654,22 @@
 	for _, file := range files {
 		untypedFiles = append(untypedFiles, string(file))
 	}
-	c.Client.diagnosticsMu.Lock()
-	defer c.Client.diagnosticsMu.Unlock()
+	c.client.diagnosticsMu.Lock()
+	defer c.client.diagnosticsMu.Unlock()
 
-	c.Client.diagnosticsDone = make(chan struct{})
+	c.client.diagnosticsDone = make(chan struct{})
 	_, err := c.Server.NonstandardRequest(ctx, "gopls/diagnoseFiles", map[string]interface{}{"files": untypedFiles})
 	if err != nil {
-		close(c.Client.diagnosticsDone)
+		close(c.client.diagnosticsDone)
 		return err
 	}
 
-	<-c.Client.diagnosticsDone
+	<-c.client.diagnosticsDone
 	return nil
 }
 
 func (c *connection) terminate(ctx context.Context) {
-	if strings.HasPrefix(c.Client.app.Remote, "internal@") {
+	if strings.HasPrefix(c.client.app.Remote, "internal@") {
 		// internal connections need to be left alive for the next test
 		return
 	}
diff --git a/gopls/internal/lsp/cmd/serve.go b/gopls/internal/lsp/cmd/serve.go
index df42e79..03cc187 100644
--- a/gopls/internal/lsp/cmd/serve.go
+++ b/gopls/internal/lsp/cmd/serve.go
@@ -90,7 +90,6 @@
 		}
 		defer closeLog()
 		di.ServerAddress = s.Address
-		di.MonitorMemory(ctx)
 		di.Serve(ctx, s.Debug)
 	}
 	var ss jsonrpc2.StreamServer
diff --git a/gopls/internal/lsp/cmd/stats.go b/gopls/internal/lsp/cmd/stats.go
index 1b9df2f..a681c5c 100644
--- a/gopls/internal/lsp/cmd/stats.go
+++ b/gopls/internal/lsp/cmd/stats.go
@@ -9,9 +9,11 @@
 	"encoding/json"
 	"flag"
 	"fmt"
+	"go/token"
 	"io/fs"
 	"os"
 	"path/filepath"
+	"reflect"
 	"runtime"
 	"strings"
 	"sync"
@@ -24,10 +26,13 @@
 	"golang.org/x/tools/gopls/internal/lsp/filecache"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
+	"golang.org/x/tools/internal/event"
 )
 
 type stats struct {
 	app *Application
+
+	Anon bool `flag:"anon" help:"hide any fields that may contain user names, file names, or source code"`
 }
 
 func (s *stats) Name() string      { return "stats" }
@@ -41,14 +46,17 @@
 workspace information relevant to performance. As a side effect, this command
 populates the gopls file cache for the current workspace.
 
+By default, this command may include output that refers to the location or
+content of user code. When the -anon flag is set, fields that may refer to user
+code are hidden.
+
 Example:
-  $ gopls stats
+  $ gopls stats -anon
 `)
 	printFlagDefaults(f)
 }
 
 func (s *stats) Run(ctx context.Context, args ...string) error {
-
 	// This undocumented environment variable allows
 	// the cmd integration test to trigger a call to bug.Report.
 	if msg := os.Getenv("TEST_GOPLS_BUG"); msg != "" {
@@ -65,9 +73,14 @@
 		return fmt.Errorf("the stats subcommand does not work with -remote")
 	}
 
+	if !s.app.Verbose {
+		event.SetExporter(nil) // don't log errors to stderr
+	}
+
 	stats := GoplsStats{
 		GOOS:         runtime.GOOS,
 		GOARCH:       runtime.GOARCH,
+		GOPLSCACHE:   os.Getenv("GOPLSCACHE"),
 		GoVersion:    runtime.Version(),
 		GoplsVersion: debug.Version,
 	}
@@ -138,10 +151,10 @@
 
 	// Gather bug reports produced by any process using
 	// this executable and persisted in the cache.
-	stats.BugReports = []string{} // non-nil for JSON
 	do("Gathering bug reports", func() error {
-		for _, report := range filecache.BugReports() {
-			stats.BugReports = append(stats.BugReports, string(report))
+		stats.CacheDir, stats.BugReports = filecache.BugReports()
+		if stats.BugReports == nil {
+			stats.BugReports = []goplsbug.Bug{} // non-nil for JSON
 		}
 		return nil
 	})
@@ -183,24 +196,51 @@
 		return err
 	}
 
-	data, err := json.MarshalIndent(stats, "", "  ")
+	// Filter JSON output to fields that are consistent with s.Anon.
+	okFields := make(map[string]interface{})
+	{
+		v := reflect.ValueOf(stats)
+		t := v.Type()
+		for i := 0; i < t.NumField(); i++ {
+			f := t.Field(i)
+			if !token.IsExported(f.Name) {
+				continue
+			}
+			if s.Anon && f.Tag.Get("anon") != "ok" {
+				// Fields that can be served with -anon must be explicitly marked as OK.
+				continue
+			}
+			vf := v.FieldByName(f.Name)
+			okFields[f.Name] = vf.Interface()
+		}
+	}
+	data, err := json.MarshalIndent(okFields, "", "  ")
 	if err != nil {
 		return err
 	}
+
 	os.Stdout.Write(data)
 	fmt.Println()
 	return nil
 }
 
+// GoplsStats holds information extracted from a gopls session in the current
+// workspace.
+//
+// Fields that should be printed with the -anon flag should be explicitly
+// marked as `anon:"ok"`. Only fields that cannot refer to user files or code
+// should be marked as such.
 type GoplsStats struct {
-	GOOS, GOARCH                 string
-	GoVersion                    string
-	GoplsVersion                 string
-	InitialWorkspaceLoadDuration string // in time.Duration string form
-	BugReports                   []string
-	MemStats                     command.MemStatsResult
-	WorkspaceStats               command.WorkspaceStatsResult
-	DirStats                     dirStats
+	GOOS, GOARCH                 string `anon:"ok"`
+	GOPLSCACHE                   string
+	GoVersion                    string `anon:"ok"`
+	GoplsVersion                 string `anon:"ok"`
+	InitialWorkspaceLoadDuration string `anon:"ok"` // in time.Duration string form
+	CacheDir                     string
+	BugReports                   []goplsbug.Bug
+	MemStats                     command.MemStatsResult       `anon:"ok"`
+	WorkspaceStats               command.WorkspaceStatsResult `anon:"ok"`
+	DirStats                     dirStats                     `anon:"ok"`
 }
 
 type dirStats struct {
diff --git a/gopls/internal/lsp/cmd/suggested_fix.go b/gopls/internal/lsp/cmd/suggested_fix.go
index 1128688..169d6d1 100644
--- a/gopls/internal/lsp/cmd/suggested_fix.go
+++ b/gopls/internal/lsp/cmd/suggested_fix.go
@@ -9,7 +9,6 @@
 	"flag"
 	"fmt"
 	"io/ioutil"
-	"log"
 	"os"
 
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
@@ -42,16 +41,11 @@
 	printFlagDefaults(f)
 }
 
-const DebugSuggestedFixEnvVar = "_DEBUG_SUGGESTED_FIX"
-
 // Run performs diagnostic checks on the file specified and either;
 // - if -w is specified, updates the file in place;
 // - if -d is specified, prints out unified diffs of the changes; or
 // - otherwise, prints the new versions to stdout.
 func (s *suggestedFix) Run(ctx context.Context, args ...string) error {
-	// For debugging golang/go#59475, enable some additional output.
-	var debug = os.Getenv(DebugSuggestedFixEnvVar) == "true"
-
 	if len(args) < 1 {
 		return tool.CommandLineErrorf("fix expects at least 1 argument")
 	}
@@ -77,12 +71,9 @@
 		return err
 	}
 	diagnostics := []protocol.Diagnostic{} // LSP wants non-nil slice
-	conn.Client.filesMu.Lock()
+	conn.client.filesMu.Lock()
 	diagnostics = append(diagnostics, file.diagnostics...)
-	conn.Client.filesMu.Unlock()
-	if debug {
-		log.Printf("file diagnostics: %#v", diagnostics)
-	}
+	conn.client.filesMu.Unlock()
 
 	// Request code actions
 	codeActionKinds := []protocol.CodeActionKind{protocol.QuickFix}
@@ -106,9 +97,6 @@
 	if err != nil {
 		return fmt.Errorf("%v: %v", from, err)
 	}
-	if debug {
-		log.Printf("code actions: %#v", actions)
-	}
 
 	// Gather edits from matching code actions.
 	var edits []protocol.TextEdit
diff --git a/gopls/internal/lsp/cmd/test/integration_test.go b/gopls/internal/lsp/cmd/test/integration_test.go
index c95790c..52ecb23 100644
--- a/gopls/internal/lsp/cmd/test/integration_test.go
+++ b/gopls/internal/lsp/cmd/test/integration_test.go
@@ -756,6 +756,18 @@
 		}
 	}
 
+	// Check that -anon suppresses fields containing user information.
+	{
+		res2 := gopls(t, tree, "stats", "-anon")
+		res2.checkExit(true)
+		var stats2 cmd.GoplsStats
+		if err := json.Unmarshal([]byte(res2.stdout), &stats2); err != nil {
+			t.Fatalf("failed to unmarshal JSON output of stats command: %v", err)
+		}
+		if got := len(stats2.BugReports); got > 0 {
+			t.Errorf("Got %d bug reports with -anon, want 0. Reports:%+v", got, stats2.BugReports)
+		}
+	}
 }
 
 // TestFix tests the 'fix' subcommand (../suggested_fix.go).
@@ -890,10 +902,7 @@
 	}
 
 	goplsCmd := exec.Command(os.Args[0], args...)
-	goplsCmd.Env = append(os.Environ(),
-		"ENTRYPOINT=goplsMain",
-		fmt.Sprintf("%s=true", cmd.DebugSuggestedFixEnvVar),
-	)
+	goplsCmd.Env = append(os.Environ(), "ENTRYPOINT=goplsMain")
 	goplsCmd.Env = append(goplsCmd.Env, env...)
 	goplsCmd.Dir = dir
 	goplsCmd.Stdout = new(bytes.Buffer)
diff --git a/gopls/internal/lsp/cmd/usage/stats.hlp b/gopls/internal/lsp/cmd/usage/stats.hlp
index 7694e29..71cce07 100644
--- a/gopls/internal/lsp/cmd/usage/stats.hlp
+++ b/gopls/internal/lsp/cmd/usage/stats.hlp
@@ -7,5 +7,11 @@
 workspace information relevant to performance. As a side effect, this command
 populates the gopls file cache for the current workspace.
 
+By default, this command may include output that refers to the location or
+content of user code. When the -anon flag is set, fields that may refer to user
+code are hidden.
+
 Example:
-  $ gopls stats
+  $ gopls stats -anon
+  -anon
+    	hide any fields that may contain user names, file names, or source code
diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go
index 8658ba55..8e817b8 100644
--- a/gopls/internal/lsp/code_action.go
+++ b/gopls/internal/lsp/code_action.go
@@ -473,6 +473,17 @@
 
 func codeActionsMatchingDiagnostics(ctx context.Context, snapshot source.Snapshot, pdiags []protocol.Diagnostic, sdiags []*source.Diagnostic) ([]protocol.CodeAction, error) {
 	var actions []protocol.CodeAction
+	var unbundled []protocol.Diagnostic // diagnostics without bundled code actions in their Data field
+	for _, pd := range pdiags {
+		bundled := source.BundledQuickFixes(pd)
+		if len(bundled) > 0 {
+			actions = append(actions, bundled...)
+		} else {
+			// No bundled actions: keep searching for a match.
+			unbundled = append(unbundled, pd)
+		}
+	}
+
 	for _, sd := range sdiags {
 		var diag *protocol.Diagnostic
 		for _, pd := range pdiags {
diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go
index 7236087..7bbadc1 100644
--- a/gopls/internal/lsp/command.go
+++ b/gopls/internal/lsp/command.go
@@ -360,10 +360,9 @@
 		progress: "Removing dependency",
 		forURI:   args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
-		// If the module is tidied apart from the one unused diagnostic, we can
-		// run `go get module@none`, and then run `go mod tidy`. Otherwise, we
-		// must make textual edits.
-		// TODO(rstambler): In Go 1.17+, we will be able to use the go command
+		// See the documentation for OnlyDiagnostic.
+		//
+		// TODO(rfindley): In Go 1.17+, we will be able to use the go command
 		// without checking if the module is tidy.
 		if args.OnlyDiagnostic {
 			return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI.SpanURI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go
index 1342e84..ababac6 100644
--- a/gopls/internal/lsp/command/interface.go
+++ b/gopls/internal/lsp/command/interface.go
@@ -236,7 +236,10 @@
 	// The go.mod file URI.
 	URI protocol.DocumentURI
 	// The module path to remove.
-	ModulePath     string
+	ModulePath string
+	// If the module is tidied apart from the one unused diagnostic, we can
+	// run `go get module@none`, and then run `go mod tidy`. Otherwise, we
+	// must make textual edits.
 	OnlyDiagnostic bool
 }
 
diff --git a/gopls/internal/lsp/debug/info.go b/gopls/internal/lsp/debug/info.go
index 34fe216..fec4562 100644
--- a/gopls/internal/lsp/debug/info.go
+++ b/gopls/internal/lsp/debug/info.go
@@ -10,6 +10,7 @@
 	"encoding/json"
 	"fmt"
 	"io"
+	"os"
 	"reflect"
 	"runtime"
 	"runtime/debug"
@@ -67,6 +68,7 @@
 	section(w, HTML, "Server Instance", func() {
 		fmt.Fprintf(w, "Start time: %v\n", i.StartTime)
 		fmt.Fprintf(w, "LogFile: %s\n", i.Logfile)
+		fmt.Fprintf(w, "pid: %d\n", os.Getpid())
 		fmt.Fprintf(w, "Working directory: %s\n", i.Workdir)
 		fmt.Fprintf(w, "Address: %s\n", i.ServerAddress)
 		fmt.Fprintf(w, "Debug address: %s\n", i.DebugAddress())
diff --git a/gopls/internal/lsp/debug/serve.go b/gopls/internal/lsp/debug/serve.go
index 3c17dad..f36a238 100644
--- a/gopls/internal/lsp/debug/serve.go
+++ b/gopls/internal/lsp/debug/serve.go
@@ -5,7 +5,6 @@
 package debug
 
 import (
-	"archive/zip"
 	"bytes"
 	"context"
 	"errors"
@@ -20,7 +19,6 @@
 	"path"
 	"path/filepath"
 	"runtime"
-	rpprof "runtime/pprof"
 	"strconv"
 	"strings"
 	"sync"
@@ -494,65 +492,6 @@
 	return i.listenedDebugAddress
 }
 
-// MonitorMemory starts recording memory statistics each second.
-func (i *Instance) MonitorMemory(ctx context.Context) {
-	tick := time.NewTicker(time.Second)
-	nextThresholdGiB := uint64(1)
-	go func() {
-		for {
-			<-tick.C
-			var mem runtime.MemStats
-			runtime.ReadMemStats(&mem)
-			if mem.HeapAlloc < nextThresholdGiB*1<<30 {
-				continue
-			}
-			if err := i.writeMemoryDebug(nextThresholdGiB, true); err != nil {
-				event.Error(ctx, "writing memory debug info", err)
-			}
-			if err := i.writeMemoryDebug(nextThresholdGiB, false); err != nil {
-				event.Error(ctx, "writing memory debug info", err)
-			}
-			event.Log(ctx, fmt.Sprintf("Wrote memory usage debug info to %v", os.TempDir()))
-			nextThresholdGiB++
-		}
-	}()
-}
-
-func (i *Instance) writeMemoryDebug(threshold uint64, withNames bool) error {
-	suffix := "withnames"
-	if !withNames {
-		suffix = "nonames"
-	}
-
-	filename := fmt.Sprintf("gopls.%d-%dGiB-%s.zip", os.Getpid(), threshold, suffix)
-	zipf, err := os.OpenFile(filepath.Join(os.TempDir(), filename), os.O_CREATE|os.O_RDWR, 0644)
-	if err != nil {
-		return err
-	}
-	zipw := zip.NewWriter(zipf)
-
-	f, err := zipw.Create("heap.pb.gz")
-	if err != nil {
-		return err
-	}
-	if err := rpprof.Lookup("heap").WriteTo(f, 0); err != nil {
-		return err
-	}
-
-	f, err = zipw.Create("goroutines.txt")
-	if err != nil {
-		return err
-	}
-	if err := rpprof.Lookup("goroutine").WriteTo(f, 1); err != nil {
-		return err
-	}
-
-	if err := zipw.Close(); err != nil {
-		return err
-	}
-	return zipf.Close()
-}
-
 func makeGlobalExporter(stderr io.Writer) event.Exporter {
 	p := export.Printer{}
 	var pMu sync.Mutex
diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go
index 90c2232..88008d3 100644
--- a/gopls/internal/lsp/diagnostics.go
+++ b/gopls/internal/lsp/diagnostics.go
@@ -145,6 +145,9 @@
 		fmt.Fprintf(h, "range: %s\n", d.Range)
 		fmt.Fprintf(h, "severity: %s\n", d.Severity)
 		fmt.Fprintf(h, "source: %s\n", d.Source)
+		if d.BundledFixes != nil {
+			fmt.Fprintf(h, "fixes: %s\n", *d.BundledFixes)
+		}
 	}
 	return fmt.Sprintf("%x", h.Sum(nil))
 }
@@ -771,6 +774,7 @@
 			Source:             string(diag.Source),
 			Tags:               emptySliceDiagnosticTag(diag.Tags),
 			RelatedInformation: diag.Related,
+			Data:               diag.BundledFixes,
 		}
 		if diag.Code != "" {
 			pdiag.Code = diag.Code
diff --git a/gopls/internal/lsp/fake/client.go b/gopls/internal/lsp/fake/client.go
index b619ef5..555428e 100644
--- a/gopls/internal/lsp/fake/client.go
+++ b/gopls/internal/lsp/fake/client.go
@@ -94,9 +94,8 @@
 	results := make([]interface{}, len(p.Items))
 	for i, item := range p.Items {
 		if item.Section == "gopls" {
-			c.editor.mu.Lock()
-			results[i] = c.editor.settingsLocked()
-			c.editor.mu.Unlock()
+			config := c.editor.Config()
+			results[i] = makeSettings(c.editor.sandbox, config)
 		}
 	}
 	return results, nil
diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go
index ae9338d..45def8f 100644
--- a/gopls/internal/lsp/fake/editor.go
+++ b/gopls/internal/lsp/fake/editor.go
@@ -37,7 +37,6 @@
 	serverConn jsonrpc2.Conn
 	client     *Client
 	sandbox    *Sandbox
-	defaultEnv map[string]string
 
 	// TODO(adonovan): buffers should be keyed by protocol.DocumentURI.
 	mu                 sync.Mutex
@@ -75,8 +74,14 @@
 // source.UserOptions, but we use a separate type here so that we expose only
 // that configuration which we support.
 //
-// The zero value for EditorConfig should correspond to its defaults.
+// The zero value for EditorConfig is the default configuration.
 type EditorConfig struct {
+	// ClientName sets the clientInfo.name for the LSP session (in the initialize request).
+	//
+	// Since this can only be set during initialization, changing this field via
+	// Editor.ChangeConfiguration has no effect.
+	ClientName string
+
 	// Env holds environment variables to apply on top of the default editor
 	// environment. When applying these variables, the special string
 	// $SANDBOX_WORKDIR is replaced by the absolute path to the sandbox working
@@ -109,10 +114,9 @@
 // NewEditor creates a new Editor.
 func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor {
 	return &Editor{
-		buffers:    make(map[string]buffer),
-		sandbox:    sandbox,
-		defaultEnv: sandbox.GoEnv(),
-		config:     config,
+		buffers: make(map[string]buffer),
+		sandbox: sandbox,
+		config:  config,
 	}
 }
 
@@ -198,19 +202,17 @@
 	return e.client
 }
 
-// settingsLocked builds the settings map for use in LSP settings RPCs.
-//
-// e.mu must be held while calling this function.
-func (e *Editor) settingsLocked() map[string]interface{} {
+// makeSettings builds the settings map for use in LSP settings RPCs.
+func makeSettings(sandbox *Sandbox, config EditorConfig) map[string]interface{} {
 	env := make(map[string]string)
-	for k, v := range e.defaultEnv {
+	for k, v := range sandbox.GoEnv() {
 		env[k] = v
 	}
-	for k, v := range e.config.Env {
+	for k, v := range config.Env {
 		env[k] = v
 	}
 	for k, v := range env {
-		v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", e.sandbox.Workdir.RootURI().SpanURI().Filename())
+		v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", sandbox.Workdir.RootURI().SpanURI().Filename())
 		env[k] = v
 	}
 
@@ -226,7 +228,7 @@
 		"completionBudget": "10s",
 	}
 
-	for k, v := range e.config.Settings {
+	for k, v := range config.Settings {
 		if k == "env" {
 			panic("must not provide env via the EditorConfig.Settings field: use the EditorConfig.Env field instead")
 		}
@@ -237,20 +239,22 @@
 }
 
 func (e *Editor) initialize(ctx context.Context) error {
+	config := e.Config()
+
 	params := &protocol.ParamInitialize{}
-	params.ClientInfo = &protocol.Msg_XInitializeParams_clientInfo{}
-	params.ClientInfo.Name = "fakeclient"
-	params.ClientInfo.Version = "v1.0.0"
-	e.mu.Lock()
-	params.WorkspaceFolders = e.makeWorkspaceFoldersLocked()
-	params.InitializationOptions = e.settingsLocked()
-	e.mu.Unlock()
-	params.Capabilities.Workspace.Configuration = true
-	params.Capabilities.Window.WorkDoneProgress = true
+	if e.config.ClientName != "" {
+		params.ClientInfo = &protocol.Msg_XInitializeParams_clientInfo{}
+		params.ClientInfo.Name = e.config.ClientName
+		params.ClientInfo.Version = "v1.0.0"
+	}
+	params.InitializationOptions = makeSettings(e.sandbox, config)
+	params.WorkspaceFolders = makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders)
+	params.Capabilities.Workspace.Configuration = true // support workspace/configuration
+	params.Capabilities.Window.WorkDoneProgress = true // support window/workDoneProgress
 
-	// TODO: set client capabilities
+	// TODO(rfindley): set client capabilities (note from the future: why?)
+
 	params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated}
-
 	params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true
 	params.Capabilities.TextDocument.SemanticTokens.Requests.Full.Value = true
 	// copied from lsp/semantic.go to avoid import cycle in tests
@@ -269,11 +273,12 @@
 	// but really we should test both ways for older editors.
 	params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = true
 
-	// This is a bit of a hack, since the fake editor doesn't actually support
-	// watching changed files that match a specific glob pattern. However, the
-	// editor does send didChangeWatchedFiles notifications, so set this to
-	// true.
+	// Glob pattern watching is enabled.
 	params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true
+
+	// "rename" operations are used for package renaming.
+	//
+	// TODO(rfindley): add support for other resource operations (create, delete, ...)
 	params.Capabilities.Workspace.WorkspaceEdit = &protocol.WorkspaceEditClientCapabilities{
 		ResourceOperations: []protocol.ResourceOperationKind{
 			"rename",
@@ -300,18 +305,15 @@
 	return nil
 }
 
-// makeWorkspaceFoldersLocked creates a slice of workspace folders to use for
+// makeWorkspaceFolders creates a slice of workspace folders to use for
 // this editing session, based on the editor configuration.
-//
-// e.mu must be held while calling this function.
-func (e *Editor) makeWorkspaceFoldersLocked() (folders []protocol.WorkspaceFolder) {
-	paths := e.config.WorkspaceFolders
+func makeWorkspaceFolders(sandbox *Sandbox, paths []string) (folders []protocol.WorkspaceFolder) {
 	if len(paths) == 0 {
-		paths = append(paths, string(e.sandbox.Workdir.RelativeTo))
+		paths = []string{string(sandbox.Workdir.RelativeTo)}
 	}
 
 	for _, path := range paths {
-		uri := string(e.sandbox.Workdir.URI(path))
+		uri := string(sandbox.Workdir.URI(path))
 		folders = append(folders, protocol.WorkspaceFolder{
 			URI:  uri,
 			Name: filepath.Base(uri),
@@ -1329,14 +1331,18 @@
 	return e.config
 }
 
+func (e *Editor) SetConfig(cfg EditorConfig) {
+	e.mu.Lock()
+	e.config = cfg
+	e.mu.Unlock()
+}
+
 // ChangeConfiguration sets the new editor configuration, and if applicable
 // sends a didChangeConfiguration notification.
 //
 // An error is returned if the change notification failed to send.
 func (e *Editor) ChangeConfiguration(ctx context.Context, newConfig EditorConfig) error {
-	e.mu.Lock()
-	e.config = newConfig
-	e.mu.Unlock() // don't hold e.mu during server calls
+	e.SetConfig(newConfig)
 	if e.Server != nil {
 		var params protocol.DidChangeConfigurationParams // empty: gopls ignores the Settings field
 		if err := e.Server.DidChangeConfiguration(ctx, &params); err != nil {
@@ -1351,12 +1357,13 @@
 //
 // The given folders must all be unique.
 func (e *Editor) ChangeWorkspaceFolders(ctx context.Context, folders []string) error {
+	config := e.Config()
+
 	// capture existing folders so that we can compute the change.
-	e.mu.Lock()
-	oldFolders := e.makeWorkspaceFoldersLocked()
-	e.config.WorkspaceFolders = folders
-	newFolders := e.makeWorkspaceFoldersLocked()
-	e.mu.Unlock()
+	oldFolders := makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders)
+	newFolders := makeWorkspaceFolders(e.sandbox, folders)
+	config.WorkspaceFolders = folders
+	e.SetConfig(config)
 
 	if e.Server == nil {
 		return nil
diff --git a/gopls/internal/lsp/filecache/filecache.go b/gopls/internal/lsp/filecache/filecache.go
index c4e2ce4..df84693 100644
--- a/gopls/internal/lsp/filecache/filecache.go
+++ b/gopls/internal/lsp/filecache/filecache.go
@@ -23,25 +23,23 @@
 import (
 	"bytes"
 	"crypto/sha256"
-	"encoding/binary"
 	"encoding/hex"
+	"encoding/json"
 	"errors"
 	"fmt"
-	"hash/crc32"
 	"io"
 	"io/fs"
 	"log"
 	"os"
 	"path/filepath"
-	"runtime"
 	"sort"
+	"strings"
 	"sync"
 	"sync/atomic"
 	"time"
 
 	"golang.org/x/tools/gopls/internal/bug"
 	"golang.org/x/tools/gopls/internal/lsp/lru"
-	"golang.org/x/tools/internal/lockedfile"
 )
 
 // Start causes the filecache to initialize and start garbage gollection.
@@ -77,60 +75,60 @@
 	iolimit <- struct{}{}        // acquire a token
 	defer func() { <-iolimit }() // release a token
 
-	name, err := filename(kind, key)
+	// Read the index file, which provides the name of the CAS file.
+	indexName, err := filename(kind, key)
 	if err != nil {
 		return nil, err
 	}
-	data, err := lockedfile.Read(name)
+	indexData, err := os.ReadFile(indexName)
 	if err != nil {
 		if errors.Is(err, os.ErrNotExist) {
 			return nil, ErrNotFound
 		}
 		return nil, err
 	}
-
-	// Verify that the Write was complete
-	// by checking the recorded length.
-	if len(data) < 8+4 {
-		return nil, ErrNotFound // cache entry is incomplete
-	}
-	length, value, checksum := data[:8], data[8:len(data)-4], data[len(data)-4:]
-	if binary.LittleEndian.Uint64(length) != uint64(len(value)) {
-		return nil, ErrNotFound // cache entry is incomplete (or too long!)
+	var valueHash [32]byte
+	if copy(valueHash[:], indexData) != len(valueHash) {
+		return nil, ErrNotFound // index entry has wrong length
 	}
 
-	// Check for corruption and print the entire file content; see
-	// issue #59289. TODO(adonovan): stop printing the entire file
-	// once we've seen enough reports to understand the pattern.
-	if binary.LittleEndian.Uint32(checksum) != crc32.ChecksumIEEE(value) {
-		// Darwin has repeatedly displayed a problem (#59895)
-		// whereby the checksum portion (and only it) is zero,
-		// which suggests a bug in its file system . Don't
-		// panic, but keep an eye on other failures for now.
-		errorf := bug.Errorf
-		if binary.LittleEndian.Uint32(checksum) == 0 && runtime.GOOS == "darwin" {
-			errorf = fmt.Errorf
-		}
-
-		return nil, errorf("internal error in filecache.Get(%q, %x): invalid checksum at end of %d-byte file %s:\n%q",
-			kind, key, len(data), name, data)
+	// Read the CAS file and check its contents match.
+	//
+	// This ensures integrity in all cases (corrupt or truncated
+	// file, short read, I/O error, wrong length, etc) except an
+	// engineered hash collision, which is infeasible.
+	casName, err := filename(casKind, valueHash)
+	if err != nil {
+		return nil, err
+	}
+	value, _ := os.ReadFile(casName) // ignore error
+	if sha256.Sum256(value) != valueHash {
+		return nil, ErrNotFound // CAS file is missing or has wrong contents
 	}
 
-	// Update file time for use by LRU eviction.
-	// (This turns every read into a write operation.
-	// If this is a performance problem, we should
-	// touch the files aynchronously.)
+	// Update file times used by LRU eviction.
+	//
+	// Because this turns a read into a write operation,
+	// we follow the approach used in the go command's
+	// cache and update the access time only if the
+	// existing timestamp is older than one hour.
 	//
 	// (Traditionally the access time would be updated
 	// automatically, but for efficiency most POSIX systems have
 	// for many years set the noatime mount option to avoid every
 	// open or read operation entailing a metadata write.)
 	now := time.Now()
-	if err := os.Chtimes(name, now, now); err != nil {
-		return nil, fmt.Errorf("failed to update access time: %w", err)
+	touch := func(filename string) {
+		st, err := os.Stat(filename)
+		if err == nil && now.Sub(st.ModTime()) > time.Hour {
+			os.Chtimes(filename, now, now) // ignore error
+		}
 	}
+	touch(indexName)
+	touch(casName)
 
 	memCache.Set(memKey{kind, key}, value, len(value))
+
 	return value, nil
 }
 
@@ -145,56 +143,81 @@
 	iolimit <- struct{}{}        // acquire a token
 	defer func() { <-iolimit }() // release a token
 
-	name, err := filename(kind, key)
+	// First, add the value to the content-
+	// addressable store (CAS), if not present.
+	hash := sha256.Sum256(value)
+	casName, err := filename(casKind, hash)
 	if err != nil {
 		return err
 	}
-	if err := os.MkdirAll(filepath.Dir(name), 0700); err != nil {
-		return err
+	// Does CAS file exist and have correct (complete) content?
+	// TODO(adonovan): opt: use mmap for this check.
+	if prev, _ := os.ReadFile(casName); !bytes.Equal(prev, value) {
+		if err := os.MkdirAll(filepath.Dir(casName), 0700); err != nil {
+			return err
+		}
+		// Avoiding O_TRUNC here is merely an optimization to avoid
+		// cache misses when two threads race to write the same file.
+		if err := writeFileNoTrunc(casName, value, 0600); err != nil {
+			os.Remove(casName) // ignore error
+			return err         // e.g. disk full
+		}
 	}
 
-	// In the unlikely event of a short write (e.g. ENOSPC)
-	// followed by process termination (e.g. a power cut), we
-	// don't want a reader to see a short file, so we record
-	// the expected length first and verify it in Get.
-	var length [8]byte
-	binary.LittleEndian.PutUint64(length[:], uint64(len(value)))
+	// Now write an index entry that refers to the CAS file.
+	indexName, err := filename(kind, key)
+	if err != nil {
+		return err
+	}
+	if err := os.MkdirAll(filepath.Dir(indexName), 0700); err != nil {
+		return err
+	}
+	if err := writeFileNoTrunc(indexName, hash[:], 0600); err != nil {
+		os.Remove(indexName) // ignore error
+		return err           // e.g. disk full
+	}
 
-	// Occasional file corruption (presence of zero bytes in JSON
-	// files) has been reported on macOS (see issue #59289),
-	// assumed due to a nonatomicity problem in the file system.
-	// Ideally the macOS kernel would be fixed, or lockedfile
-	// would implement a workaround (since its job is to provide
-	// reliable the mutual exclusion primitive that allows
-	// cooperating gopls processes to implement transactional
-	// file replacement), but for now we add an extra integrity
-	// check: a 32-bit checksum at the end.
-	var checksum [4]byte
-	binary.LittleEndian.PutUint32(checksum[:], crc32.ChecksumIEEE(value))
-
-	// Windows doesn't support atomic rename--we tried MoveFile,
-	// MoveFileEx, ReplaceFileEx, and SetFileInformationByHandle
-	// of RenameFileInfo, all to no avail--so instead we use
-	// advisory file locking, which is only about 2x slower even
-	// on POSIX platforms with atomic rename.
-	return lockedfile.Write(name, io.MultiReader(
-		bytes.NewReader(length[:]),
-		bytes.NewReader(value),
-		bytes.NewReader(checksum[:])),
-		0600)
+	return nil
 }
 
+// writeFileNoTrunc is like os.WriteFile but doesn't truncate until
+// after the write, so that racing writes of the same data are idempotent.
+func writeFileNoTrunc(filename string, data []byte, perm os.FileMode) error {
+	f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, perm)
+	if err != nil {
+		return err
+	}
+	_, err = f.Write(data)
+	if err == nil {
+		err = f.Truncate(int64(len(data)))
+	}
+	if closeErr := f.Close(); err == nil {
+		err = closeErr
+	}
+	return err
+}
+
+// reserved kind strings
+const (
+	casKind = "cas" // content-addressable store files
+	bugKind = "bug" // gopls bug reports
+)
+
 var iolimit = make(chan struct{}, 128) // counting semaphore to limit I/O concurrency in Set.
 
 var budget int64 = 1e9 // 1GB
 
-// SetBudget sets a soft limit on disk usage of the cache (in bytes)
-// and returns the previous value. Supplying a negative value queries
-// the current value without changing it.
+// SetBudget sets a soft limit on disk usage of files in the cache (in
+// bytes) and returns the previous value. Supplying a negative value
+// queries the current value without changing it.
 //
 // If two gopls processes have different budgets, the one with the
 // lower budget will collect garbage more actively, but both will
 // observe the effect.
+//
+// Even in the steady state, the storage usage reported by the 'du'
+// command may exceed the budget by as much as 50-70% due to the
+// overheads of directories and the effects of block quantization.
 func SetBudget(new int64) (old int64) {
 	if new < 0 {
 		return atomic.LoadInt64(&budget)
@@ -204,22 +227,62 @@
 
 // --- implementation ----
 
-// filename returns the cache entry of the specified kind and key.
+// filename returns the name of the cache file of the specified kind and key.
 //
-// A typical cache entry is a file name such as:
+// A typical cache file has a name such as:
 //
-//	$HOME/Library/Caches / gopls / VVVVVVVV / kind / KK / KKKK...KKKK
+//	$HOME/Library/Caches / gopls / VVVVVVVV / KK / KKKK...KKKK - kind
 //
 // The portions separated by spaces are as follows:
 // - The user's preferred cache directory; the default value varies by OS.
 // - The constant "gopls".
 // - The "version", 32 bits of the digest of the gopls executable.
-// - The kind or purpose of this cache subtree (e.g. "analysis").
 // - The first 8 bits of the key, to avoid huge directories.
 // - The full 256 bits of the key.
+// - The kind or purpose of this cache file (e.g. "analysis").
 //
-// Once a file is written its contents are never modified, though it
-// may be atomically replaced or removed.
+// The kind establishes a namespace for the keys. It is represented as
+// a suffix, not a segment, as this significantly reduces the number
+// of directories created, and thus the storage overhead.
+//
+// Previous iterations of the design aimed for the invariant that once
+// a file is written, its contents are never modified, though it may
+// be atomically replaced or removed. However, not all platforms have
+// an atomic rename operation (our first approach), and file locking
+// (our second) is a notoriously fickle mechanism.
+//
+// The current design instead exploits a trick from the cache
+// implementation used by the go command: writes of small files are in
+// practice atomic (all or nothing) on all platforms.
+// (See GOROOT/src/cmd/go/internal/cache/cache.go.)
+//
+// Russ Cox notes: "all file systems use an rwlock around every file
+// system block, including data blocks, so any writes or reads within
+// the same block are going to be handled atomically by the FS
+// implementation without any need to request file locking explicitly.
+// And since the files are so small, there's only one block. (A block
+// is at minimum 512 bytes, usually much more.)" And: "all modern file
+// systems protect against [partial writes due to power loss] with
+// journals."
+//
+// We use a two-level scheme consisting of an index and a
+// content-addressable store (CAS). A single cache entry consists of
+// two files. The value of a cache entry is written into the file at
+// filename("cas", sha256(value)). Since the value may be arbitrarily
+// large, this write is not atomic. That means we must check the
+// integrity of the contents read back from the CAS to make sure they
+// hash to the expected key. If the CAS file is incomplete or
+// inconsistent, we proceed as if it were missing.
+//
+// Once the CAS file has been written, we write a small fixed-size
+// index file at filename(kind, key), using the values supplied by the
+// caller. The index file contains the hash that identifies the value
+// file in the CAS. (We could add extra metadata to this file, up to
+// 512B, the minimum size of a disk block, if later desired, so long
+// as the total size remains fixed.) Because the index file is small,
+// concurrent writes to it are atomic in practice, even though this is
+// not guaranteed by any OS. The fixed size ensures that readers can't
+// see a palimpsest when a short new file overwrites a longer old one.
 //
 // New versions of gopls are free to reorganize the contents of the
 // version directory as needs evolve.  But all versions of gopls must
@@ -230,12 +293,13 @@
 // after older ones: in the development cycle especially, new
 // new versions may be created frequently.
 func filename(kind string, key [32]byte) (string, error) {
-	hex := fmt.Sprintf("%x", key)
+	base := fmt.Sprintf("%x-%s", key, kind)
 	dir, err := getCacheDir()
 	if err != nil {
 		return "", err
 	}
-	return filepath.Join(dir, kind, hex[:2], hex), nil
+	// Keep the BugReports function consistent with this one.
+	return filepath.Join(dir, base[:2], base), nil
 }
 
 // getCacheDir returns the persistent cache directory of all processes
@@ -462,8 +526,6 @@
 	}
 }
 
-const bugKind = "bug" // reserved kind for gopls bug reports
-
 func init() {
 	// Register a handler to durably record this process's first
 	// assertion failure in the cache so that we can ask users to
@@ -472,37 +534,51 @@
 		// Wait for cache init (bugs in tests happen early).
 		_, _ = getCacheDir()
 
-		value := []byte(fmt.Sprintf("%s: %+v", time.Now().Format(time.RFC3339), bug))
-		key := sha256.Sum256(value)
-		_ = Set(bugKind, key, value)
+		data, err := json.Marshal(bug)
+		if err != nil {
+			panic(fmt.Sprintf("error marshalling bug %+v: %v", bug, err))
+		}
+
+		key := sha256.Sum256(data)
+		_ = Set(bugKind, key, data)
 	})
 }
 
 // BugReports returns a new unordered array of the contents
 // of all cached bug reports produced by this executable.
-func BugReports() [][]byte {
+// It also returns the location of the cache directory
+// used by this process (or "" on initialization error).
+func BugReports() (string, []bug.Bug) {
+	// To test this logic, run:
+	// $ TEST_GOPLS_BUG=oops gopls stats   # trigger a bug
+	// $ gopls stats                       # list the bugs
+
 	dir, err := getCacheDir()
 	if err != nil {
-		return nil // ignore initialization errors
+		return "", nil // ignore initialization errors
 	}
-	var result [][]byte
-	_ = filepath.Walk(filepath.Join(dir, bugKind),
-		func(path string, info fs.FileInfo, err error) error {
-			if err != nil {
-				return nil // ignore readdir/stat errors
+	var result []bug.Bug
+	_ = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
+		if err != nil {
+			return nil // ignore readdir/stat errors
+		}
+		// Parse the key from each "XXXX-bug" cache file name.
+		if !info.IsDir() && strings.HasSuffix(path, bugKind) {
+			var key [32]byte
+			n, err := hex.Decode(key[:], []byte(filepath.Base(path)[:len(key)*2]))
+			if err != nil || n != len(key) {
+				return nil // ignore malformed file names
 			}
-			if !info.IsDir() {
-				var key [32]byte
-				n, err := hex.Decode(key[:], []byte(filepath.Base(path)))
-				if err != nil || n != len(key) {
-					return nil // ignore malformed file names
+			content, err := Get(bugKind, key)
+			if err == nil { // ignore read errors
+				var b bug.Bug
+				if err := json.Unmarshal(content, &b); err != nil {
+					log.Printf("error marshalling bug %q: %v", string(content), err)
 				}
-				content, err := Get(bugKind, key)
-				if err == nil { // ignore read errors
-					result = append(result, content)
-				}
+				result = append(result, b)
 			}
-			return nil
-		})
-	return result
+		}
+		return nil
+	})
+	return dir, result
 }
diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go
index 9d12f97..7486f24 100644
--- a/gopls/internal/lsp/general.go
+++ b/gopls/internal/lsp/general.go
@@ -8,6 +8,7 @@
 	"context"
 	"encoding/json"
 	"fmt"
+	"go/build"
 	"log"
 	"os"
 	"path"
@@ -59,7 +60,7 @@
 	if err := s.handleOptionResults(ctx, source.SetOptions(options, params.InitializationOptions)); err != nil {
 		return nil, err
 	}
-	options.ForClientCapabilities(params.Capabilities)
+	options.ForClientCapabilities(params.ClientInfo, params.Capabilities)
 
 	if options.ShowBugReports {
 		// Report the next bug that occurs on the server.
@@ -239,14 +240,16 @@
 
 // GoVersionTable maps Go versions to the gopls version in which support will
 // be deprecated, and the final gopls version supporting them without warnings.
-// Keep this in sync with gopls/README.md
+// Keep this in sync with gopls/README.md.
 //
 // Must be sorted in ascending order of Go version.
 //
 // Mutable for testing.
 var GoVersionTable = []GoVersionSupport{
 	{12, "", "v0.7.5"},
-	{15, "v0.11.0", "v0.9.5"},
+	{15, "", "v0.9.5"},
+	{16, "v0.13.0", "v0.11.0"},
+	{17, "v0.13.0", "v0.11.0"},
 }
 
 // GoVersionSupport holds information about end-of-life Go version support.
@@ -262,11 +265,13 @@
 	return GoVersionTable[len(GoVersionTable)-1].GoVersion + 1
 }
 
-// versionMessage returns the warning/error message to display if the user is
-// on the given Go version, if any. The goVersion variable is the X in Go 1.X.
+// versionMessage returns the warning/error message to display if the user has
+// the given Go version, if any. The goVersion variable is the X in Go 1.X. If
+// fromBuild is set, the Go version is the version used to build gopls.
+// Otherwise, it is the go command version.
 //
 // If goVersion is invalid (< 0), it returns "", 0.
-func versionMessage(goVersion int) (string, protocol.MessageType) {
+func versionMessage(goVersion int, fromBuild bool) (string, protocol.MessageType) {
 	if goVersion < 0 {
 		return "", 0
 	}
@@ -276,7 +281,11 @@
 			var msgBuilder strings.Builder
 
 			mType := protocol.Error
-			fmt.Fprintf(&msgBuilder, "Found Go version 1.%d", goVersion)
+			if fromBuild {
+				fmt.Fprintf(&msgBuilder, "Gopls was built with Go version 1.%d", goVersion)
+			} else {
+				fmt.Fprintf(&msgBuilder, "Found Go version 1.%d", goVersion)
+			}
 			if v.DeprecatedVersion != "" {
 				// not deprecated yet, just a warning
 				fmt.Fprintf(&msgBuilder, ", which will be unsupported by gopls %s. ", v.DeprecatedVersion)
@@ -299,15 +308,15 @@
 //
 // It should be called after views change.
 func (s *Server) checkViewGoVersions() {
-	oldestVersion := -1
+	oldestVersion, fromBuild := go1Point(), true
 	for _, view := range s.session.Views() {
 		viewVersion := view.GoVersion()
 		if oldestVersion == -1 || viewVersion < oldestVersion {
-			oldestVersion = viewVersion
+			oldestVersion, fromBuild = viewVersion, false
 		}
 	}
 
-	if msg, mType := versionMessage(oldestVersion); msg != "" {
+	if msg, mType := versionMessage(oldestVersion, fromBuild); msg != "" {
 		s.eventuallyShowMessage(context.Background(), &protocol.ShowMessageParams{
 			Type:    mType,
 			Message: msg,
@@ -315,6 +324,21 @@
 	}
 }
 
+// go1Point returns the x in Go 1.x. If an error occurs extracting the go
+// version, it returns -1.
+//
+// Copied from the testenv package.
+func go1Point() int {
+	for i := len(build.Default.ReleaseTags) - 1; i >= 0; i-- {
+		var version int
+		if _, err := fmt.Sscanf(build.Default.ReleaseTags[i], "go1.%d", &version); err != nil {
+			continue
+		}
+		return version
+	}
+	return -1
+}
+
 func (s *Server) addFolders(ctx context.Context, folders []protocol.WorkspaceFolder) error {
 	originalViews := len(s.session.Views())
 	viewErrors := make(map[span.URI]error)
@@ -445,14 +469,13 @@
 
 // registerWatchedDirectoriesLocked sends the workspace/didChangeWatchedFiles
 // registrations to the client and updates s.watchedDirectories.
+// The caller must not subsequently mutate patterns.
 func (s *Server) registerWatchedDirectoriesLocked(ctx context.Context, patterns map[string]struct{}) error {
 	if !s.session.Options().DynamicWatchedFilesSupported {
 		return nil
 	}
-	for k := range s.watchedGlobPatterns {
-		delete(s.watchedGlobPatterns, k)
-	}
-	watchers := []protocol.FileSystemWatcher{} // must be a slice
+	s.watchedGlobPatterns = patterns
+	watchers := make([]protocol.FileSystemWatcher, 0, len(patterns)) // must be a slice
 	val := protocol.WatchChange | protocol.WatchDelete | protocol.WatchCreate
 	for pattern := range patterns {
 		watchers = append(watchers, protocol.FileSystemWatcher{
@@ -473,10 +496,6 @@
 		return err
 	}
 	s.watchRegistrationCount++
-
-	for k, v := range patterns {
-		s.watchedGlobPatterns[k] = v
-	}
 	return nil
 }
 
diff --git a/gopls/internal/lsp/general_test.go b/gopls/internal/lsp/general_test.go
index a0312ba..6bc0dc1 100644
--- a/gopls/internal/lsp/general_test.go
+++ b/gopls/internal/lsp/general_test.go
@@ -14,18 +14,22 @@
 func TestVersionMessage(t *testing.T) {
 	tests := []struct {
 		goVersion    int
+		fromBuild    bool
 		wantContains []string // string fragments that we expect to see
 		wantType     protocol.MessageType
 	}{
-		{-1, nil, 0},
-		{12, []string{"1.12", "not supported", "upgrade to Go 1.16", "install gopls v0.7.5"}, protocol.Error},
-		{13, []string{"1.13", "will be unsupported by gopls v0.11.0", "upgrade to Go 1.16", "install gopls v0.9.5"}, protocol.Warning},
-		{15, []string{"1.15", "will be unsupported by gopls v0.11.0", "upgrade to Go 1.16", "install gopls v0.9.5"}, protocol.Warning},
-		{16, nil, 0},
+		{-1, false, nil, 0},
+		{12, false, []string{"1.12", "not supported", "upgrade to Go 1.18", "install gopls v0.7.5"}, protocol.Error},
+		{13, false, []string{"1.13", "not supported", "upgrade to Go 1.18", "install gopls v0.9.5"}, protocol.Error},
+		{15, false, []string{"1.15", "not supported", "upgrade to Go 1.18", "install gopls v0.9.5"}, protocol.Error},
+		{15, true, []string{"Gopls was built with Go version 1.15", "not supported", "upgrade to Go 1.18", "install gopls v0.9.5"}, protocol.Error},
+		{16, false, []string{"1.16", "will be unsupported by gopls v0.13.0", "upgrade to Go 1.18", "install gopls v0.11.0"}, protocol.Warning},
+		{17, false, []string{"1.17", "will be unsupported by gopls v0.13.0", "upgrade to Go 1.18", "install gopls v0.11.0"}, protocol.Warning},
+		{17, true, []string{"Gopls was built with Go version 1.17", "will be unsupported by gopls v0.13.0", "upgrade to Go 1.18", "install gopls v0.11.0"}, protocol.Warning},
 	}
 
 	for _, test := range tests {
-		gotMsg, gotType := versionMessage(test.goVersion)
+		gotMsg, gotType := versionMessage(test.goVersion, test.fromBuild)
 
 		if len(test.wantContains) == 0 && gotMsg != "" {
 			t.Errorf("versionMessage(%d) = %q, want \"\"", test.goVersion, gotMsg)
diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go
index af5fe38..cd1c85b 100644
--- a/gopls/internal/lsp/mod/diagnostics.go
+++ b/gopls/internal/lsp/mod/diagnostics.go
@@ -9,11 +9,14 @@
 import (
 	"context"
 	"fmt"
+	"runtime"
 	"sort"
 	"strings"
+	"sync"
 
 	"golang.org/x/mod/modfile"
 	"golang.org/x/mod/semver"
+	"golang.org/x/sync/errgroup"
 	"golang.org/x/tools/gopls/internal/govulncheck"
 	"golang.org/x/tools/gopls/internal/lsp/command"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
@@ -58,24 +61,36 @@
 }
 
 func collectDiagnostics(ctx context.Context, snapshot source.Snapshot, diagFn func(context.Context, source.Snapshot, source.FileHandle) ([]*source.Diagnostic, error)) (map[span.URI][]*source.Diagnostic, error) {
+
+	g, ctx := errgroup.WithContext(ctx)
+	cpulimit := runtime.GOMAXPROCS(0)
+	g.SetLimit(cpulimit)
+
+	var mu sync.Mutex
 	reports := make(map[span.URI][]*source.Diagnostic)
+
 	for _, uri := range snapshot.ModFiles() {
-		fh, err := snapshot.ReadFile(ctx, uri)
-		if err != nil {
-			return nil, err
-		}
-		reports[fh.URI()] = []*source.Diagnostic{}
-		diagnostics, err := diagFn(ctx, snapshot, fh)
-		if err != nil {
-			return nil, err
-		}
-		for _, d := range diagnostics {
-			fh, err := snapshot.ReadFile(ctx, d.URI)
+		uri := uri
+		g.Go(func() error {
+			fh, err := snapshot.ReadFile(ctx, uri)
 			if err != nil {
-				return nil, err
+				return err
 			}
-			reports[fh.URI()] = append(reports[fh.URI()], d)
-		}
+			diagnostics, err := diagFn(ctx, snapshot, fh)
+			if err != nil {
+				return err
+			}
+			for _, d := range diagnostics {
+				mu.Lock()
+				reports[d.URI] = append(reports[fh.URI()], d)
+				mu.Unlock()
+			}
+			return nil
+		})
+	}
+
+	if err := g.Wait(); err != nil {
+		return nil, err
 	}
 	return reports, nil
 }
diff --git a/gopls/internal/lsp/protocol/generate/tables.go b/gopls/internal/lsp/protocol/generate/tables.go
index 126301a..8fb9707 100644
--- a/gopls/internal/lsp/protocol/generate/tables.go
+++ b/gopls/internal/lsp/protocol/generate/tables.go
@@ -68,6 +68,7 @@
 	{"Command", "arguments"}:       "[]json.RawMessage",
 	{"CompletionItem", "textEdit"}: "TextEdit",
 	{"Diagnostic", "code"}:         "interface{}",
+	{"Diagnostic", "data"}:         "json.RawMessage", // delay unmarshalling quickfixes
 
 	{"DocumentDiagnosticReportPartialResult", "relatedDocuments"}: "map[DocumentURI]interface{}",
 
diff --git a/gopls/internal/lsp/protocol/tsprotocol.go b/gopls/internal/lsp/protocol/tsprotocol.go
index 8469aeb..f8ebb46 100644
--- a/gopls/internal/lsp/protocol/tsprotocol.go
+++ b/gopls/internal/lsp/protocol/tsprotocol.go
@@ -896,7 +896,7 @@
 	// notification and `textDocument/codeAction` request.
 	//
 	// @since 3.16.0
-	Data interface{} `json:"data,omitempty"`
+	Data *json.RawMessage `json:"data,omitempty"`
 }
 
 // Client capabilities specific to diagnostic pull requests.
diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go
index 9d9f023..a770616 100644
--- a/gopls/internal/lsp/regtest/expectation.go
+++ b/gopls/internal/lsp/regtest/expectation.go
@@ -235,7 +235,7 @@
 	}
 	return Expectation{
 		Check:       check,
-		Description: "received ShowMessage",
+		Description: fmt.Sprintf("received window/showMessage containing %q", containing),
 	}
 }
 
@@ -576,50 +576,6 @@
 	return jsonProperty(m[path[0]], path[1:]...)
 }
 
-// RegistrationMatching asserts that the client has received a capability
-// registration matching the given regexp.
-//
-// TODO(rfindley): remove this once TestWatchReplaceTargets has been revisited.
-//
-// Deprecated: use (No)FileWatchMatching
-func RegistrationMatching(re string) Expectation {
-	rec := regexp.MustCompile(re)
-	check := func(s State) Verdict {
-		for _, p := range s.registrations {
-			for _, r := range p.Registrations {
-				if rec.Match([]byte(r.Method)) {
-					return Met
-				}
-			}
-		}
-		return Unmet
-	}
-	return Expectation{
-		Check:       check,
-		Description: fmt.Sprintf("registration matching %q", re),
-	}
-}
-
-// UnregistrationMatching asserts that the client has received an
-// unregistration whose ID matches the given regexp.
-func UnregistrationMatching(re string) Expectation {
-	rec := regexp.MustCompile(re)
-	check := func(s State) Verdict {
-		for _, p := range s.unregistrations {
-			for _, r := range p.Unregisterations {
-				if rec.Match([]byte(r.Method)) {
-					return Met
-				}
-			}
-		}
-		return Unmet
-	}
-	return Expectation{
-		Check:       check,
-		Description: fmt.Sprintf("unregistration matching %q", re),
-	}
-}
-
 // Diagnostics asserts that there is at least one diagnostic matching the given
 // filters.
 func Diagnostics(filters ...DiagnosticFilter) Expectation {
diff --git a/gopls/internal/lsp/regtest/options.go b/gopls/internal/lsp/regtest/options.go
index 7a41696..f55fd5b 100644
--- a/gopls/internal/lsp/regtest/options.go
+++ b/gopls/internal/lsp/regtest/options.go
@@ -64,8 +64,14 @@
 	})
 }
 
-// Settings is a RunOption that sets user-provided configuration for the LSP
-// server.
+// ClientName sets the LSP client name.
+func ClientName(name string) RunOption {
+	return optionSetter(func(opts *runConfig) {
+		opts.editor.ClientName = name
+	})
+}
+
+// Settings sets user-provided configuration for the LSP server.
 //
 // As a special case, the env setting must not be provided via Settings: use
 // EnvVars instead.
diff --git a/gopls/internal/lsp/server.go b/gopls/internal/lsp/server.go
index 9f82e90..db69565 100644
--- a/gopls/internal/lsp/server.go
+++ b/gopls/internal/lsp/server.go
@@ -29,7 +29,7 @@
 	return &Server{
 		diagnostics:           map[span.URI]*fileReports{},
 		gcOptimizationDetails: make(map[source.PackageID]struct{}),
-		watchedGlobPatterns:   make(map[string]struct{}),
+		watchedGlobPatterns:   nil, // empty
 		changedFiles:          make(map[span.URI]struct{}),
 		session:               session,
 		client:                client,
@@ -85,6 +85,7 @@
 	// watchedGlobPatterns is the set of glob patterns that we have requested
 	// the client watch on disk. It will be updated as the set of directories
 	// that the server should watch changes.
+	// The map field may be reassigned but the map is immutable.
 	watchedGlobPatternsMu  sync.Mutex
 	watchedGlobPatterns    map[string]struct{}
 	watchRegistrationCount int
diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go
index 281772b..f777fdb 100644
--- a/gopls/internal/lsp/source/api_json.go
+++ b/gopls/internal/lsp/source/api_json.go
@@ -760,7 +760,7 @@
 			Command: "gopls.remove_dependency",
 			Title:   "Remove a dependency",
 			Doc:     "Removes a dependency from the go.mod file of a module.",
-			ArgDoc:  "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// The module path to remove.\n\t\"ModulePath\": string,\n\t\"OnlyDiagnostic\": bool,\n}",
+			ArgDoc:  "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// The module path to remove.\n\t\"ModulePath\": string,\n\t// If the module is tidied apart from the one unused diagnostic, we can\n\t// run `go get module@none`, and then run `go mod tidy`. Otherwise, we\n\t// must make textual edits.\n\t\"OnlyDiagnostic\": bool,\n}",
 		},
 		{
 			Command: "gopls.reset_go_mod_diagnostics",
diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go
index ad5ce16..bc2b0c3 100644
--- a/gopls/internal/lsp/source/completion/completion.go
+++ b/gopls/internal/lsp/source/completion/completion.go
@@ -12,6 +12,7 @@
 	"go/ast"
 	"go/constant"
 	"go/parser"
+	"go/printer"
 	"go/scanner"
 	"go/token"
 	"go/types"
@@ -1268,19 +1269,30 @@
 				var sn snippet.Builder
 				sn.WriteText(id.Name)
 				sn.WriteText("(")
+
+				var cfg printer.Config // slight overkill
 				var nparams int
-				for _, field := range fn.Type.Params.List {
-					if field.Names != nil {
-						nparams += len(field.Names)
-					} else {
-						nparams++
-					}
-				}
-				for i := 0; i < nparams; i++ {
-					if i > 0 {
+				param := func(name string, typ ast.Expr) {
+					if nparams > 0 {
 						sn.WriteText(", ")
 					}
-					sn.WritePlaceholder(nil)
+					nparams++
+					sn.WritePlaceholder(func(b *snippet.Builder) {
+						var buf strings.Builder
+						buf.WriteString(name)
+						buf.WriteByte(' ')
+						cfg.Fprint(&buf, token.NewFileSet(), typ)
+						b.WriteText(buf.String())
+					})
+				}
+				for _, field := range fn.Type.Params.List {
+					if field.Names != nil {
+						for _, name := range field.Names {
+							param(name.Name, field.Type)
+						}
+					} else {
+						param("_", field.Type)
+					}
 				}
 				sn.WriteText(")")
 				item.snippet = &sn
diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go
index fc08dcf..336e35b 100644
--- a/gopls/internal/lsp/source/diagnostics.go
+++ b/gopls/internal/lsp/source/diagnostics.go
@@ -6,7 +6,9 @@
 
 import (
 	"context"
+	"encoding/json"
 
+	"golang.org/x/tools/gopls/internal/bug"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/span"
 )
@@ -136,3 +138,81 @@
 
 	*outT = append(*outT, tdiags...)
 }
+
+// quickFixesJSON is a JSON-serializable list of quick fixes
+// to be saved in the protocol.Diagnostic.Data field.
+type quickFixesJSON struct {
+	// TODO(rfindley): pack some sort of identifier here for later
+	// lookup/validation?
+	Fixes []protocol.CodeAction
+}
+
+// BundleQuickFixes attempts to bundle sd.SuggestedFixes into the
+// sd.BundledFixes field, so that it can be round-tripped through the client.
+// It returns false if the quick-fixes cannot be bundled.
+func BundleQuickFixes(sd *Diagnostic) bool {
+	if len(sd.SuggestedFixes) == 0 {
+		return true
+	}
+	var actions []protocol.CodeAction
+	for _, fix := range sd.SuggestedFixes {
+		if fix.Edits != nil {
+			// For now, we only support bundled code actions that execute commands.
+			//
+			// In order to cleanly support bundled edits, we'd have to guarantee that
+			// the edits were generated on the current snapshot. But this naively
+			// implies that every fix would have to include a snapshot ID, which
+			// would require us to republish all diagnostics on each new snapshot.
+			//
+			// TODO(rfindley): in order to avoid this additional chatter, we'd need
+			// to build some sort of registry or other mechanism on the snapshot to
+			// check whether a diagnostic is still valid.
+			return false
+		}
+		action := protocol.CodeAction{
+			Title:   fix.Title,
+			Kind:    fix.ActionKind,
+			Command: fix.Command,
+		}
+		actions = append(actions, action)
+	}
+	fixes := quickFixesJSON{
+		Fixes: actions,
+	}
+	data, err := json.Marshal(fixes)
+	if err != nil {
+		bug.Reportf("marshalling quick fixes: %v", err)
+		return false
+	}
+	msg := json.RawMessage(data)
+	sd.BundledFixes = &msg
+	return true
+}
+
+// BundledQuickFixes extracts any bundled codeActions from the
+// diag.Data field.
+func BundledQuickFixes(diag protocol.Diagnostic) []protocol.CodeAction {
+	if diag.Data == nil {
+		return nil
+	}
+	var fix quickFixesJSON
+	if err := json.Unmarshal(*diag.Data, &fix); err != nil {
+		bug.Reportf("unmarshalling quick fix: %v", err)
+		return nil
+	}
+
+	var actions []protocol.CodeAction
+	for _, action := range fix.Fixes {
+		// See BundleQuickFixes: for now we only support bundling commands.
+		if action.Edit != nil {
+			bug.Reportf("bundled fix %q includes workspace edits", action.Title)
+			continue
+		}
+		// associate the action with the incoming diagnostic
+		// (Note that this does not mutate the fix.Fixes slice).
+		action.Diagnostics = []protocol.Diagnostic{diag}
+		actions = append(actions, action)
+	}
+
+	return actions
+}
diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go
index 2ca8895..23d6e9a 100644
--- a/gopls/internal/lsp/source/options.go
+++ b/gopls/internal/lsp/source/options.go
@@ -169,6 +169,7 @@
 				DeepCompletion:          true,
 				ChattyDiagnostics:       true,
 				NewDiff:                 "both",
+				SubdirWatchPatterns:     SubdirWatchPatternsAuto,
 			},
 			Hooks: Hooks{
 				// TODO(adonovan): switch to new diff.Strings implementation.
@@ -198,6 +199,7 @@
 // ClientOptions holds LSP-specific configuration that is provided by the
 // client.
 type ClientOptions struct {
+	ClientInfo                                 *protocol.Msg_XInitializeParams_clientInfo
 	InsertTextFormat                           protocol.InsertTextFormat
 	ConfigurationSupported                     bool
 	DynamicConfigurationSupported              bool
@@ -536,6 +538,9 @@
 // average user. These may be settings used by tests or outdated settings that
 // will soon be deprecated. Some of these settings may not even be configurable
 // by the user.
+//
+// TODO(rfindley): even though these settings are not intended for
+// modification, we should surface them in our documentation.
 type InternalOptions struct {
 	// LiteralCompletions controls whether literal candidates such as
 	// "&someStruct{}" are offered. Tests disable this flag to simplify
@@ -599,8 +604,42 @@
 	// file change. If unset, gopls only reports diagnostics when they change, or
 	// when a file is opened or closed.
 	ChattyDiagnostics bool
+
+	// SubdirWatchPatterns configures the file watching glob patterns registered
+	// by gopls.
+	//
+	// Some clients (namely VS Code) do not send workspace/didChangeWatchedFile
+	// notifications for files contained in a directory when that directory is
+	// deleted:
+	// https://github.com/microsoft/vscode/issues/109754
+	//
+	// In this case, gopls would miss important notifications about deleted
+	// packages. To work around this, gopls registers a watch pattern for each
+	// directory containing Go files.
+	//
+	// Unfortunately, other clients experience performance problems with this
+	// many watch patterns, so there is no single behavior that works well for
+	// all clients.
+	//
+	// The "subdirWatchPatterns" setting allows configuring this behavior. Its
+	// default value of "auto" attempts to guess the correct behavior based on
+	// the client name. We'd love to avoid this specialization, but as described
+	// above there is no single value that works for all clients.
+	//
+	// If any LSP client does not behave well with the default value (for
+	// example, if like VS Code it drops file notifications), please file an
+	// issue.
+	SubdirWatchPatterns SubdirWatchPatterns
 }
 
+type SubdirWatchPatterns string
+
+const (
+	SubdirWatchPatternsOn   SubdirWatchPatterns = "on"
+	SubdirWatchPatternsOff  SubdirWatchPatterns = "off"
+	SubdirWatchPatternsAuto SubdirWatchPatterns = "auto"
+)
+
 type ImportShortcut string
 
 const (
@@ -742,7 +781,8 @@
 	return results
 }
 
-func (o *Options) ForClientCapabilities(caps protocol.ClientCapabilities) {
+func (o *Options) ForClientCapabilities(clientName *protocol.Msg_XInitializeParams_clientInfo, caps protocol.ClientCapabilities) {
+	o.ClientInfo = clientName
 	// Check if the client supports snippets in completion items.
 	if caps.Workspace.WorkspaceEdit != nil {
 		o.SupportedResourceOperations = caps.Workspace.WorkspaceEdit.ResourceOperations
@@ -1159,6 +1199,15 @@
 	case "chattyDiagnostics":
 		result.setBool(&o.ChattyDiagnostics)
 
+	case "subdirWatchPatterns":
+		if s, ok := result.asOneOf(
+			string(SubdirWatchPatternsOn),
+			string(SubdirWatchPatternsOff),
+			string(SubdirWatchPatternsAuto),
+		); ok {
+			o.SubdirWatchPatterns = SubdirWatchPatterns(s)
+		}
+
 	// Replaced settings.
 	case "experimentalDisabledAnalyses":
 		result.deprecated("analyses")
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index 6dd3811..2f7a3f2 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -8,6 +8,7 @@
 	"bytes"
 	"context"
 	"crypto/sha256"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"go/ast"
@@ -971,7 +972,18 @@
 	Related []protocol.DiagnosticRelatedInformation
 
 	// Fields below are used internally to generate quick fixes. They aren't
-	// part of the LSP spec and don't leave the server.
+	// part of the LSP spec and historically didn't leave the server.
+	//
+	// Update(2023-05): version 3.16 of the LSP spec included support for the
+	// Diagnostic.data field, which holds arbitrary data preserved in the
+	// diagnostic for codeAction requests. This field allows bundling additional
+	// information for quick-fixes, and gopls can (and should) use this
+	// information to avoid re-evaluating diagnostics in code-action handlers.
+	//
+	// In order to stage this transition incrementally, the 'BundledFixes' field
+	// may store a 'bundled' (=json-serialized) form of the associated
+	// SuggestedFixes. Not all diagnostics have their fixes bundled.
+	BundledFixes   *json.RawMessage
 	SuggestedFixes []SuggestedFix
 }
 
diff --git a/gopls/internal/regtest/bench/didchange_test.go b/gopls/internal/regtest/bench/didchange_test.go
index 6bde10e..2030f32 100644
--- a/gopls/internal/regtest/bench/didchange_test.go
+++ b/gopls/internal/regtest/bench/didchange_test.go
@@ -19,11 +19,13 @@
 // shared file cache.
 var editID int64 = time.Now().UnixNano()
 
-var didChangeTests = []struct {
+type changeTest struct {
 	repo string
 	file string
-}{
-	{"google-cloud-go", "httpreplay/httpreplay.go"},
+}
+
+var didChangeTests = []changeTest{
+	{"google-cloud-go", "internal/annotate.go"},
 	{"istio", "pkg/fuzz/util.go"},
 	{"kubernetes", "pkg/controller/lookup_cache.go"},
 	{"kuma", "api/generic/insights.go"},
@@ -64,43 +66,63 @@
 
 func BenchmarkDiagnoseChange(b *testing.B) {
 	for _, test := range didChangeTests {
-		b.Run(test.repo, func(b *testing.B) {
-			sharedEnv := getRepo(b, test.repo).sharedEnv(b)
-			config := fake.EditorConfig{
-				Env: map[string]string{
-					"GOPATH": sharedEnv.Sandbox.GOPATH(),
-				},
-				Settings: map[string]interface{}{
-					"diagnosticsDelay": "0s",
-				},
-			}
-			// Use a new env to avoid the diagnostic delay: we want to measure how
-			// long it takes to produce the diagnostics.
-			env := getRepo(b, test.repo).newEnv(b, "diagnoseChange", config)
-			defer env.Close()
-			env.OpenFile(test.file)
-			// Insert the text we'll be modifying at the top of the file.
-			env.EditBuffer(test.file, protocol.TextEdit{NewText: "// __REGTEST_PLACEHOLDER_0__\n"})
-			env.AfterChange()
-			b.ResetTimer()
-
-			// We must use an extra subtest layer here, so that we only set up the
-			// shared env once (otherwise we pay additional overhead and the profiling
-			// flags don't work).
-			b.Run("diagnose", func(b *testing.B) {
-				for i := 0; i < b.N; i++ {
-					edits := atomic.AddInt64(&editID, 1)
-					env.EditBuffer(test.file, protocol.TextEdit{
-						Range: protocol.Range{
-							Start: protocol.Position{Line: 0, Character: 0},
-							End:   protocol.Position{Line: 1, Character: 0},
-						},
-						// Increment the placeholder text, to ensure cache misses.
-						NewText: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", edits),
-					})
-					env.AfterChange()
-				}
-			})
-		})
+		runChangeDiagnosticsBenchmark(b, test, false)
 	}
 }
+
+// TODO(rfindley): add a benchmark for with a metadata-affecting change, when
+// this matters.
+func BenchmarkDiagnoseSave(b *testing.B) {
+	for _, test := range didChangeTests {
+		runChangeDiagnosticsBenchmark(b, test, true)
+	}
+}
+
+// runChangeDiagnosticsBenchmark runs a benchmark to edit the test file and
+// await the resulting diagnostics pass. If save is set, the file is also saved.
+func runChangeDiagnosticsBenchmark(b *testing.B, test changeTest, save bool) {
+	b.Run(test.repo, func(b *testing.B) {
+		sharedEnv := getRepo(b, test.repo).sharedEnv(b)
+		config := fake.EditorConfig{
+			Env: map[string]string{
+				"GOPATH": sharedEnv.Sandbox.GOPATH(),
+			},
+			Settings: map[string]interface{}{
+				"diagnosticsDelay": "0s",
+			},
+		}
+		// Use a new env to avoid the diagnostic delay: we want to measure how
+		// long it takes to produce the diagnostics.
+		env := getRepo(b, test.repo).newEnv(b, "diagnoseSave", config)
+		defer env.Close()
+		env.OpenFile(test.file)
+		// Insert the text we'll be modifying at the top of the file.
+		env.EditBuffer(test.file, protocol.TextEdit{NewText: "// __REGTEST_PLACEHOLDER_0__\n"})
+		if save {
+			env.SaveBuffer(test.file)
+		}
+		env.AfterChange()
+		b.ResetTimer()
+
+		// We must use an extra subtest layer here, so that we only set up the
+		// shared env once (otherwise we pay additional overhead and the profiling
+		// flags don't work).
+		b.Run("diagnose", func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				edits := atomic.AddInt64(&editID, 1)
+				env.EditBuffer(test.file, protocol.TextEdit{
+					Range: protocol.Range{
+						Start: protocol.Position{Line: 0, Character: 0},
+						End:   protocol.Position{Line: 1, Character: 0},
+					},
+					// Increment the placeholder text, to ensure cache misses.
+					NewText: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", edits),
+				})
+				if save {
+					env.SaveBuffer(test.file)
+				}
+				env.AfterChange()
+			}
+		})
+	})
+}
diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go
index 872bdc2..7f86594 100644
--- a/gopls/internal/regtest/completion/completion_test.go
+++ b/gopls/internal/regtest/completion/completion_test.go
@@ -527,13 +527,60 @@
 		env.AcceptCompletion(loc, completions.Items[0])
 		env.Await(env.DoneWithChange())
 		got := env.BufferText("main.go")
-		want := "package main\r\n\r\nimport (\r\n\t\"fmt\"\r\n\t\"math\"\r\n)\r\n\r\nfunc main() {\r\n\tfmt.Println(\"a\")\r\n\tmath.Sqrt(${1:})\r\n}\r\n"
+		want := "package main\r\n\r\nimport (\r\n\t\"fmt\"\r\n\t\"math\"\r\n)\r\n\r\nfunc main() {\r\n\tfmt.Println(\"a\")\r\n\tmath.Sqrt(${1:x float64})\r\n}\r\n"
 		if diff := cmp.Diff(want, got); diff != "" {
 			t.Errorf("unimported completion (-want +got):\n%s", diff)
 		}
 	})
 }
 
+func TestUnimportedCompletionHasPlaceholders60269(t *testing.T) {
+	// We can't express this as a marker test because it doesn't support AcceptCompletion.
+	const src = `
+-- go.mod --
+module example.com
+go 1.12
+
+-- a/a.go --
+package a
+
+var _ = b.F
+
+-- b/b.go --
+package b
+
+func F0(a, b int, c float64) {}
+func F1(int, chan *string) {}
+`
+	WithOptions(
+		WindowsLineEndings(),
+	).Run(t, src, func(t *testing.T, env *Env) {
+		env.OpenFile("a/a.go")
+		env.Await(env.DoneWithOpen())
+
+		// The table lists the expected completions as they appear in Items.
+		const common = "package a\r\n\r\nimport \"example.com/b\"\r\n\r\nvar _ = "
+		for i, want := range []string{
+			common + "b.F0(${1:a int}, ${2:b int}, ${3:c float64})\r\n",
+			common + "b.F1(${1:_ int}, ${2:_ chan *string})\r\n",
+		} {
+			loc := env.RegexpSearch("a/a.go", "b.F()")
+			completions := env.Completion(loc)
+			if len(completions.Items) == 0 {
+				t.Fatalf("no completion items")
+			}
+			saved := env.BufferText("a/a.go")
+			env.AcceptCompletion(loc, completions.Items[i])
+			env.Await(env.DoneWithChange())
+			got := env.BufferText("a/a.go")
+			if diff := cmp.Diff(want, got); diff != "" {
+				t.Errorf("%d: unimported completion (-want +got):\n%s", i, diff)
+			}
+			env.SetBufferContent("a/a.go", saved) // restore
+		}
+	})
+}
+
 func TestPackageMemberCompletionAfterSyntaxError(t *testing.T) {
 	// This test documents the current broken behavior due to golang/go#58833.
 	const src = `
diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go
index f8e59a0..de675a5 100644
--- a/gopls/internal/regtest/diagnostics/diagnostics_test.go
+++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go
@@ -1506,7 +1506,13 @@
 	bob.Hello()
 }
 `
-	Run(t, mod, func(t *testing.T, env *Env) {
+	WithOptions(
+		Settings{
+			// Now that we don't watch subdirs by default (except for VS Code),
+			// we must explicitly ask gopls to requests subdir watch patterns.
+			"subdirWatchPatterns": "on",
+		},
+	).Run(t, mod, func(t *testing.T, env *Env) {
 		env.OnceMet(
 			InitialWorkspaceLoad,
 			FileWatchMatching("bob"),
@@ -1695,8 +1701,7 @@
 		env.OpenFile("nested/hello/hello.go")
 		env.AfterChange(
 			Diagnostics(env.AtRegexp("nested/hello/hello.go", "helloHelper")),
-			Diagnostics(env.AtRegexp("nested/hello/hello.go", "package hello"), WithMessage("nested module")),
-			OutstandingWork(lsp.WorkspaceLoadFailure, "nested module"),
+			Diagnostics(env.AtRegexp("nested/hello/hello.go", "package (hello)"), WithMessage("not included in your workspace")),
 		)
 	})
 }
diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go
index 6cbfe37..853abcd 100644
--- a/gopls/internal/regtest/misc/configuration_test.go
+++ b/gopls/internal/regtest/misc/configuration_test.go
@@ -57,9 +57,7 @@
 //
 // Gopls should not get confused about buffer content when recreating the view.
 func TestMajorOptionsChange(t *testing.T) {
-	t.Skip("broken due to golang/go#57934")
-
-	testenv.NeedsGo1Point(t, 17)
+	testenv.NeedsGo1Point(t, 19) // needs staticcheck
 
 	const files = `
 -- go.mod --
diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go
index 42aa372..b5da9b0 100644
--- a/gopls/internal/regtest/misc/failures_test.go
+++ b/gopls/internal/regtest/misc/failures_test.go
@@ -15,7 +15,6 @@
 // that includes a line directive, which makes no difference since
 // gopls ignores line directives.
 func TestHoverFailure(t *testing.T) {
-	t.Skip("line directives //line ")
 	const mod = `
 -- go.mod --
 module mod.com
@@ -48,7 +47,6 @@
 // This test demonstrates a case where gopls is not at all confused by
 // line directives, because it completely ignores them.
 func TestFailingDiagnosticClearingOnEdit(t *testing.T) {
-	t.Skip("line directives //line ")
 	// badPackageDup contains a duplicate definition of the 'a' const.
 	// This is a minor variant of TestDiagnosticClearingOnEdit from
 	// diagnostics_test.go, with a line directive, which makes no difference.
diff --git a/gopls/internal/regtest/misc/hover_test.go b/gopls/internal/regtest/misc/hover_test.go
index 24ee6d8..41c6529 100644
--- a/gopls/internal/regtest/misc/hover_test.go
+++ b/gopls/internal/regtest/misc/hover_test.go
@@ -84,12 +84,6 @@
 }
 
 func TestHoverIntLiteral(t *testing.T) {
-	// TODO(rfindley): this behavior doesn't actually make sense for vars. It is
-	// misleading to format their value when it is (of course) variable.
-	//
-	// Instead, we should allow hovering on numeric literals.
-	t.Skip("golang/go#58220: broken due to new hover logic")
-
 	const source = `
 -- main.go --
 package main
@@ -106,13 +100,13 @@
 	Run(t, source, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
 		hexExpected := "58190"
-		got, _ := env.Hover(env.RegexpSearch("main.go", "hex"))
+		got, _ := env.Hover(env.RegexpSearch("main.go", "0xe"))
 		if got != nil && !strings.Contains(got.Value, hexExpected) {
 			t.Errorf("Hover: missing expected field '%s'. Got:\n%q", hexExpected, got.Value)
 		}
 
 		binExpected := "73"
-		got, _ = env.Hover(env.RegexpSearch("main.go", "bigBin"))
+		got, _ = env.Hover(env.RegexpSearch("main.go", "0b1"))
 		if got != nil && !strings.Contains(got.Value, binExpected) {
 			t.Errorf("Hover: missing expected field '%s'. Got:\n%q", binExpected, got.Value)
 		}
diff --git a/gopls/internal/regtest/misc/leak_test.go b/gopls/internal/regtest/misc/leak_test.go
deleted file mode 100644
index 586ffcc..0000000
--- a/gopls/internal/regtest/misc/leak_test.go
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright 2022 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 (
-	"context"
-	"testing"
-
-	"github.com/google/go-cmp/cmp"
-	"golang.org/x/tools/gopls/internal/hooks"
-	"golang.org/x/tools/gopls/internal/lsp/cache"
-	"golang.org/x/tools/gopls/internal/lsp/debug"
-	"golang.org/x/tools/gopls/internal/lsp/fake"
-	"golang.org/x/tools/gopls/internal/lsp/lsprpc"
-	. "golang.org/x/tools/gopls/internal/lsp/regtest"
-	"golang.org/x/tools/internal/jsonrpc2"
-	"golang.org/x/tools/internal/jsonrpc2/servertest"
-)
-
-// Test for golang/go#57222.
-func TestCacheLeak(t *testing.T) {
-	// TODO(rfindley): either fix this test with additional instrumentation, or
-	// delete it.
-	t.Skip("This test races with cache eviction.")
-	const files = `-- a.go --
-package a
-
-func _() {
-	println("1")
-}
-`
-	c := cache.New(nil)
-	env := setupEnv(t, files, c)
-	env.Await(InitialWorkspaceLoad)
-	env.OpenFile("a.go")
-
-	// Make a couple edits to stabilize cache state.
-	//
-	// For some reason, after only one edit we're left with two parsed files
-	// (perhaps because something had to ParseHeader). If this test proves flaky,
-	// we'll need to investigate exactly what is causing various parse modes to
-	// be present (or rewrite the test to be more tolerant, for example make ~100
-	// modifications and assert that we're within a few of where we're started).
-	env.RegexpReplace("a.go", "1", "2")
-	env.RegexpReplace("a.go", "2", "3")
-	env.AfterChange()
-
-	// Capture cache state, make an arbitrary change, and wait for gopls to do
-	// its work. Afterward, we should have the exact same number of parsed
-	before := c.MemStats()
-	env.RegexpReplace("a.go", "3", "4")
-	env.AfterChange()
-	after := c.MemStats()
-
-	if diff := cmp.Diff(before, after); diff != "" {
-		t.Errorf("store objects differ after change (-before +after)\n%s", diff)
-	}
-}
-
-// setupEnv creates a new sandbox environment for editing the txtar encoded
-// content of files. It uses a new gopls instance backed by the Cache c.
-func setupEnv(t *testing.T, files string, c *cache.Cache) *Env {
-	ctx := debug.WithInstance(context.Background(), "", "off")
-	server := lsprpc.NewStreamServer(c, false, hooks.Options)
-	ts := servertest.NewPipeServer(server, jsonrpc2.NewRawStream)
-	s, err := fake.NewSandbox(&fake.SandboxConfig{
-		Files: fake.UnpackTxt(files),
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	a := NewAwaiter(s.Workdir)
-	const skipApplyEdits = false
-	editor, err := fake.NewEditor(s, fake.EditorConfig{}).Connect(ctx, ts, a.Hooks(), skipApplyEdits)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	return &Env{
-		T:       t,
-		Ctx:     ctx,
-		Editor:  editor,
-		Sandbox: s,
-		Awaiter: a,
-	}
-}
diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go
index 03e60ac..855141a 100644
--- a/gopls/internal/regtest/modfile/modfile_test.go
+++ b/gopls/internal/regtest/modfile/modfile_test.go
@@ -498,14 +498,8 @@
 			ReadDiagnostics("a/go.mod", &modDiags),
 		)
 
-		// golang.go#57987: now that gopls is incremental, we must be careful where
-		// we request diagnostics. We must design a simpler way to correlate
-		// published diagnostics with subsequent code action requests (see also the
-		// comment in Server.codeAction).
-		const canRequestCodeActionsForWorkspaceDiagnostics = false
-		if canRequestCodeActionsForWorkspaceDiagnostics {
-			env.ApplyQuickFixes("a/go.mod", modDiags.Diagnostics)
-			const want = `module mod.com
+		env.ApplyQuickFixes("a/go.mod", modDiags.Diagnostics)
+		const want = `module mod.com
 
 go 1.12
 
@@ -514,11 +508,10 @@
 	example.com/blah/v2 v2.0.0
 )
 `
-			env.SaveBuffer("a/go.mod")
-			env.AfterChange(NoDiagnostics(ForFile("a/main.go")))
-			if got := env.BufferText("a/go.mod"); got != want {
-				t.Fatalf("suggested fixes failed:\n%s", compare.Text(want, got))
-			}
+		env.SaveBuffer("a/go.mod")
+		env.AfterChange(NoDiagnostics(ForFile("a/main.go")))
+		if got := env.BufferText("a/go.mod"); got != want {
+			t.Fatalf("suggested fixes failed:\n%s", compare.Text(want, got))
 		}
 	})
 }
diff --git a/gopls/internal/regtest/watch/setting_test.go b/gopls/internal/regtest/watch/setting_test.go
new file mode 100644
index 0000000..9ed7fde
--- /dev/null
+++ b/gopls/internal/regtest/watch/setting_test.go
@@ -0,0 +1,85 @@
+// Copyright 2023 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 regtest
+
+import (
+	"fmt"
+	"testing"
+
+	. "golang.org/x/tools/gopls/internal/lsp/regtest"
+)
+
+func TestSubdirWatchPatterns(t *testing.T) {
+	const files = `
+-- go.mod --
+module mod.test
+
+go 1.18
+-- subdir/subdir.go --
+package subdir
+`
+
+	tests := []struct {
+		clientName          string
+		subdirWatchPatterns string
+		wantWatched         bool
+	}{
+		{"other client", "on", true},
+		{"other client", "off", false},
+		{"other client", "auto", false},
+		{"Visual Studio Code", "auto", true},
+	}
+
+	for _, test := range tests {
+		t.Run(fmt.Sprintf("%s_%s", test.clientName, test.subdirWatchPatterns), func(t *testing.T) {
+			WithOptions(
+				ClientName(test.clientName),
+				Settings{
+					"subdirWatchPatterns": test.subdirWatchPatterns,
+				},
+			).Run(t, files, func(t *testing.T, env *Env) {
+				var expectation Expectation
+				if test.wantWatched {
+					expectation = FileWatchMatching("subdir")
+				} else {
+					expectation = NoFileWatchMatching("subdir")
+				}
+				env.OnceMet(
+					InitialWorkspaceLoad,
+					expectation,
+				)
+			})
+		})
+	}
+}
+
+// This test checks that we surface errors for invalid subdir watch patterns,
+// as the triple of ("off"|"on"|"auto") may be confusing to users inclined to
+// use (true|false) or some other truthy value.
+func TestSubdirWatchPatterns_BadValues(t *testing.T) {
+	tests := []struct {
+		badValue    interface{}
+		wantMessage string
+	}{
+		{true, "invalid type bool, expect string"},
+		{false, "invalid type bool, expect string"},
+		{"yes", `invalid option "yes"`},
+	}
+
+	for _, test := range tests {
+		t.Run(fmt.Sprint(test.badValue), func(t *testing.T) {
+			WithOptions(
+				Settings{
+					"subdirWatchPatterns": test.badValue,
+				},
+			).Run(t, "", func(t *testing.T, env *Env) {
+				env.OnceMet(
+					InitialWorkspaceLoad,
+					ShownMessage(test.wantMessage),
+				)
+			})
+		})
+	}
+}
diff --git a/gopls/internal/regtest/workspace/adhoc_test.go b/gopls/internal/regtest/workspace/adhoc_test.go
new file mode 100644
index 0000000..d726242
--- /dev/null
+++ b/gopls/internal/regtest/workspace/adhoc_test.go
@@ -0,0 +1,42 @@
+// Copyright 2022 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 workspace
+
+import (
+	"testing"
+
+	. "golang.org/x/tools/gopls/internal/lsp/regtest"
+	"golang.org/x/tools/internal/testenv"
+)
+
+// Test for golang/go#57209: editing a file in an ad-hoc package should not
+// trigger conflicting diagnostics.
+func TestAdhoc_Edits(t *testing.T) {
+	testenv.NeedsGo1Point(t, 18)
+
+	const files = `
+-- a.go --
+package foo
+
+const X = 1
+
+-- b.go --
+package foo
+
+// import "errors"
+
+const Y = X
+`
+
+	Run(t, files, func(t *testing.T, env *Env) {
+		env.OpenFile("b.go")
+
+		for i := 0; i < 10; i++ {
+			env.RegexpReplace("b.go", `// import "errors"`, `import "errors"`)
+			env.RegexpReplace("b.go", `import "errors"`, `// import "errors"`)
+			env.AfterChange(NoDiagnostics())
+		}
+	})
+}
diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go
index 005a7e9..d7d54c4 100644
--- a/gopls/internal/regtest/workspace/broken_test.go
+++ b/gopls/internal/regtest/workspace/broken_test.go
@@ -23,10 +23,9 @@
 
 // Test for golang/go#53933
 func TestBrokenWorkspace_DuplicateModules(t *testing.T) {
-	testenv.NeedsGo1Point(t, 18)
-
-	// TODO(golang/go#57650): fix this feature.
-	t.Skip("we no longer detect duplicate modules")
+	// The go command error message was improved in Go 1.20 to mention multiple
+	// modules.
+	testenv.NeedsGo1Point(t, 20)
 
 	// This proxy module content is replaced by the workspace, but is still
 	// required for module resolution to function in the Go command.
@@ -98,8 +97,8 @@
 		ProxyFiles(proxy),
 	).Run(t, src, func(t *testing.T, env *Env) {
 		env.OpenFile("package1/main.go")
-		env.Await(
-			OutstandingWork(lsp.WorkspaceLoadFailure, `found module "example.com/foo" multiple times in the workspace`),
+		env.AfterChange(
+			OutstandingWork(lsp.WorkspaceLoadFailure, `module example.com/foo appears multiple times in workspace`),
 		)
 
 		// Remove the redundant vendored copy of example.com.
@@ -110,10 +109,10 @@
 			./package2/vendor/example.com/foo
 		)
 		`)
-		env.Await(NoOutstandingWork())
+		env.AfterChange(NoOutstandingWork())
 
 		// Check that definitions in package1 go to the copy vendored in package2.
-		location := env.GoToDefinition(env.RegexpSearch("package1/main.go", "CompleteMe")).URI.SpanURI().Filename()
+		location := string(env.GoToDefinition(env.RegexpSearch("package1/main.go", "CompleteMe")).URI)
 		const wantLocation = "package2/vendor/example.com/foo/foo.go"
 		if !strings.HasSuffix(location, wantLocation) {
 			t.Errorf("got definition of CompleteMe at %q, want %q", location, wantLocation)
diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go
index 5a94e42..02e3a8c 100644
--- a/gopls/internal/regtest/workspace/workspace_test.go
+++ b/gopls/internal/regtest/workspace/workspace_test.go
@@ -183,29 +183,6 @@
 	})
 }
 
-// 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) {
-	t.Skipf("skipping known-flaky test: see https://go.dev/issue/50748")
-
-	WithOptions(
-		ProxyFiles(workspaceProxy),
-		WorkspaceFolders("pkg"),
-	).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()
-		goModWithReplace := fmt.Sprintf(`%s
-replace random.org => %s
-`, env.ReadWorkspaceFile("pkg/go.mod"), dir)
-		env.WriteWorkspaceFile("pkg/go.mod", goModWithReplace)
-		env.AfterChange(
-			UnregistrationMatching("didChangeWatchedFiles"),
-			RegistrationMatching("didChangeWatchedFiles"),
-		)
-	})
-}
-
 const workspaceModuleProxy = `
 -- example.com@v1.2.3/go.mod --
 module example.com
@@ -575,10 +552,18 @@
 `
 	WithOptions(
 		ProxyFiles(workspaceModuleProxy),
+		Settings{
+			"subdirWatchPatterns": "on",
+		},
 	).Run(t, multiModule, func(t *testing.T, env *Env) {
-		// Initially, the go.work 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.
+		// Initially, the go.work should cause only the a.com module to be loaded,
+		// so we shouldn't get any file watches for modb. Further validate this by
+		// jumping to a definition in b.com and ensuring that we go to the module
+		// cache.
+		env.OnceMet(
+			InitialWorkspaceLoad,
+			NoFileWatchMatching("modb"),
+		)
 		env.OpenFile("moda/a/a.go")
 		env.Await(env.DoneWithOpen())
 
@@ -610,9 +595,13 @@
 `)
 
 		// As of golang/go#54069, writing go.work to the workspace triggers a
-		// workspace reload.
+		// workspace reload, and new file watches.
 		env.AfterChange(
 			Diagnostics(env.AtRegexp("modb/b/b.go", "x")),
+			// TODO(golang/go#60340): we don't get a file watch yet, because
+			// updateWatchedDirectories runs before snapshot.load. Instead, we get it
+			// after the next change (the didOpen below).
+			// FileWatchMatching("modb"),
 		)
 
 		// Jumping to definition should now go to b.com in the workspace.
@@ -623,7 +612,13 @@
 		// Now, let's modify the go.work *overlay* (not on disk), and verify that
 		// this change is only picked up once it is saved.
 		env.OpenFile("go.work")
-		env.AfterChange()
+		env.AfterChange(
+			// TODO(golang/go#60340): delete this expectation in favor of
+			// the commented-out expectation above, once we fix the evaluation order
+			// of file watches. We should not have to wait for a second change to get
+			// the correct watches.
+			FileWatchMatching("modb"),
+		)
 		env.SetBufferContent("go.work", `go 1.17
 
 use (
@@ -1059,7 +1054,7 @@
 		// package declaration.
 		env.AfterChange(
 			NoDiagnostics(ForFile("main.go")),
-			Diagnostics(AtPosition("b/main.go", 0, 0)),
+			Diagnostics(env.AtRegexp("b/main.go", "package (main)")),
 		)
 		env.WriteWorkspaceFile("go.work", `go 1.16
 
@@ -1085,7 +1080,7 @@
 
 		env.AfterChange(
 			NoDiagnostics(ForFile("main.go")),
-			Diagnostics(AtPosition("b/main.go", 0, 0)),
+			Diagnostics(env.AtRegexp("b/main.go", "package (main)")),
 		)
 	})
 }
diff --git a/internal/lockedfile/internal/filelock/filelock.go b/internal/lockedfile/internal/filelock/filelock.go
deleted file mode 100644
index 05f27c3..0000000
--- a/internal/lockedfile/internal/filelock/filelock.go
+++ /dev/null
@@ -1,99 +0,0 @@
-// Copyright 2018 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 filelock provides a platform-independent API for advisory file
-// locking. Calls to functions in this package on platforms that do not support
-// advisory locks will return errors for which IsNotSupported returns true.
-package filelock
-
-import (
-	"errors"
-	"io/fs"
-	"os"
-)
-
-// A File provides the minimal set of methods required to lock an open file.
-// File implementations must be usable as map keys.
-// The usual implementation is *os.File.
-type File interface {
-	// Name returns the name of the file.
-	Name() string
-
-	// Fd returns a valid file descriptor.
-	// (If the File is an *os.File, it must not be closed.)
-	Fd() uintptr
-
-	// Stat returns the FileInfo structure describing file.
-	Stat() (fs.FileInfo, error)
-}
-
-// Lock places an advisory write lock on the file, blocking until it can be
-// locked.
-//
-// If Lock returns nil, no other process will be able to place a read or write
-// lock on the file until this process exits, closes f, or calls Unlock on it.
-//
-// If f's descriptor is already read- or write-locked, the behavior of Lock is
-// unspecified.
-//
-// Closing the file may or may not release the lock promptly. Callers should
-// ensure that Unlock is always called when Lock succeeds.
-func Lock(f File) error {
-	return lock(f, writeLock)
-}
-
-// RLock places an advisory read lock on the file, blocking until it can be locked.
-//
-// If RLock returns nil, no other process will be able to place a write lock on
-// the file until this process exits, closes f, or calls Unlock on it.
-//
-// If f is already read- or write-locked, the behavior of RLock is unspecified.
-//
-// Closing the file may or may not release the lock promptly. Callers should
-// ensure that Unlock is always called if RLock succeeds.
-func RLock(f File) error {
-	return lock(f, readLock)
-}
-
-// Unlock removes an advisory lock placed on f by this process.
-//
-// The caller must not attempt to unlock a file that is not locked.
-func Unlock(f File) error {
-	return unlock(f)
-}
-
-// String returns the name of the function corresponding to lt
-// (Lock, RLock, or Unlock).
-func (lt lockType) String() string {
-	switch lt {
-	case readLock:
-		return "RLock"
-	case writeLock:
-		return "Lock"
-	default:
-		return "Unlock"
-	}
-}
-
-// IsNotSupported returns a boolean indicating whether the error is known to
-// report that a function is not supported (possibly for a specific input).
-// It is satisfied by ErrNotSupported as well as some syscall errors.
-func IsNotSupported(err error) bool {
-	return isNotSupported(underlyingError(err))
-}
-
-var ErrNotSupported = errors.New("operation not supported")
-
-// underlyingError returns the underlying error for known os error types.
-func underlyingError(err error) error {
-	switch err := err.(type) {
-	case *fs.PathError:
-		return err.Err
-	case *os.LinkError:
-		return err.Err
-	case *os.SyscallError:
-		return err.Err
-	}
-	return err
-}
diff --git a/internal/lockedfile/internal/filelock/filelock_fcntl.go b/internal/lockedfile/internal/filelock/filelock_fcntl.go
deleted file mode 100644
index 3098519..0000000
--- a/internal/lockedfile/internal/filelock/filelock_fcntl.go
+++ /dev/null
@@ -1,215 +0,0 @@
-// Copyright 2018 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.
-
-//go:build aix || (solaris && !illumos)
-// +build aix solaris,!illumos
-
-// This code implements the filelock API using POSIX 'fcntl' locks, which attach
-// to an (inode, process) pair rather than a file descriptor. To avoid unlocking
-// files prematurely when the same file is opened through different descriptors,
-// we allow only one read-lock at a time.
-//
-// Most platforms provide some alternative API, such as an 'flock' system call
-// or an F_OFD_SETLK command for 'fcntl', that allows for better concurrency and
-// does not require per-inode bookkeeping in the application.
-
-package filelock
-
-import (
-	"errors"
-	"io"
-	"io/fs"
-	"math/rand"
-	"sync"
-	"syscall"
-	"time"
-)
-
-type lockType int16
-
-const (
-	readLock  lockType = syscall.F_RDLCK
-	writeLock lockType = syscall.F_WRLCK
-)
-
-type inode = uint64 // type of syscall.Stat_t.Ino
-
-type inodeLock struct {
-	owner File
-	queue []<-chan File
-}
-
-var (
-	mu     sync.Mutex
-	inodes = map[File]inode{}
-	locks  = map[inode]inodeLock{}
-)
-
-func lock(f File, lt lockType) (err error) {
-	// POSIX locks apply per inode and process, and the lock for an inode is
-	// released when *any* descriptor for that inode is closed. So we need to
-	// synchronize access to each inode internally, and must serialize lock and
-	// unlock calls that refer to the same inode through different descriptors.
-	fi, err := f.Stat()
-	if err != nil {
-		return err
-	}
-	ino := fi.Sys().(*syscall.Stat_t).Ino
-
-	mu.Lock()
-	if i, dup := inodes[f]; dup && i != ino {
-		mu.Unlock()
-		return &fs.PathError{
-			Op:   lt.String(),
-			Path: f.Name(),
-			Err:  errors.New("inode for file changed since last Lock or RLock"),
-		}
-	}
-	inodes[f] = ino
-
-	var wait chan File
-	l := locks[ino]
-	if l.owner == f {
-		// This file already owns the lock, but the call may change its lock type.
-	} else if l.owner == nil {
-		// No owner: it's ours now.
-		l.owner = f
-	} else {
-		// Already owned: add a channel to wait on.
-		wait = make(chan File)
-		l.queue = append(l.queue, wait)
-	}
-	locks[ino] = l
-	mu.Unlock()
-
-	if wait != nil {
-		wait <- f
-	}
-
-	// Spurious EDEADLK errors arise on platforms that compute deadlock graphs at
-	// the process, rather than thread, level. Consider processes P and Q, with
-	// threads P.1, P.2, and Q.3. The following trace is NOT a deadlock, but will be
-	// reported as a deadlock on systems that consider only process granularity:
-	//
-	// 	P.1 locks file A.
-	// 	Q.3 locks file B.
-	// 	Q.3 blocks on file A.
-	// 	P.2 blocks on file B. (This is erroneously reported as a deadlock.)
-	// 	P.1 unlocks file A.
-	// 	Q.3 unblocks and locks file A.
-	// 	Q.3 unlocks files A and B.
-	// 	P.2 unblocks and locks file B.
-	// 	P.2 unlocks file B.
-	//
-	// These spurious errors were observed in practice on AIX and Solaris in
-	// cmd/go: see https://golang.org/issue/32817.
-	//
-	// We work around this bug by treating EDEADLK as always spurious. If there
-	// really is a lock-ordering bug between the interacting processes, it will
-	// become a livelock instead, but that's not appreciably worse than if we had
-	// a proper flock implementation (which generally does not even attempt to
-	// diagnose deadlocks).
-	//
-	// In the above example, that changes the trace to:
-	//
-	// 	P.1 locks file A.
-	// 	Q.3 locks file B.
-	// 	Q.3 blocks on file A.
-	// 	P.2 spuriously fails to lock file B and goes to sleep.
-	// 	P.1 unlocks file A.
-	// 	Q.3 unblocks and locks file A.
-	// 	Q.3 unlocks files A and B.
-	// 	P.2 wakes up and locks file B.
-	// 	P.2 unlocks file B.
-	//
-	// We know that the retry loop will not introduce a *spurious* livelock
-	// because, according to the POSIX specification, EDEADLK is only to be
-	// returned when “the lock is blocked by a lock from another process”.
-	// If that process is blocked on some lock that we are holding, then the
-	// resulting livelock is due to a real deadlock (and would manifest as such
-	// when using, for example, the flock implementation of this package).
-	// If the other process is *not* blocked on some other lock that we are
-	// holding, then it will eventually release the requested lock.
-
-	nextSleep := 1 * time.Millisecond
-	const maxSleep = 500 * time.Millisecond
-	for {
-		err = setlkw(f.Fd(), lt)
-		if err != syscall.EDEADLK {
-			break
-		}
-		time.Sleep(nextSleep)
-
-		nextSleep += nextSleep
-		if nextSleep > maxSleep {
-			nextSleep = maxSleep
-		}
-		// Apply 10% jitter to avoid synchronizing collisions when we finally unblock.
-		nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep))
-	}
-
-	if err != nil {
-		unlock(f)
-		return &fs.PathError{
-			Op:   lt.String(),
-			Path: f.Name(),
-			Err:  err,
-		}
-	}
-
-	return nil
-}
-
-func unlock(f File) error {
-	var owner File
-
-	mu.Lock()
-	ino, ok := inodes[f]
-	if ok {
-		owner = locks[ino].owner
-	}
-	mu.Unlock()
-
-	if owner != f {
-		panic("unlock called on a file that is not locked")
-	}
-
-	err := setlkw(f.Fd(), syscall.F_UNLCK)
-
-	mu.Lock()
-	l := locks[ino]
-	if len(l.queue) == 0 {
-		// No waiters: remove the map entry.
-		delete(locks, ino)
-	} else {
-		// The first waiter is sending us their file now.
-		// Receive it and update the queue.
-		l.owner = <-l.queue[0]
-		l.queue = l.queue[1:]
-		locks[ino] = l
-	}
-	delete(inodes, f)
-	mu.Unlock()
-
-	return err
-}
-
-// setlkw calls FcntlFlock with F_SETLKW for the entire file indicated by fd.
-func setlkw(fd uintptr, lt lockType) error {
-	for {
-		err := syscall.FcntlFlock(fd, syscall.F_SETLKW, &syscall.Flock_t{
-			Type:   int16(lt),
-			Whence: io.SeekStart,
-			Start:  0,
-			Len:    0, // All bytes.
-		})
-		if err != syscall.EINTR {
-			return err
-		}
-	}
-}
-
-func isNotSupported(err error) bool {
-	return err == syscall.ENOSYS || err == syscall.ENOTSUP || err == syscall.EOPNOTSUPP || err == ErrNotSupported
-}
diff --git a/internal/lockedfile/internal/filelock/filelock_other.go b/internal/lockedfile/internal/filelock/filelock_other.go
deleted file mode 100644
index cde868f..0000000
--- a/internal/lockedfile/internal/filelock/filelock_other.go
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright 2018 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.
-
-//go:build !(aix || darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd || solaris) && !plan9 && !windows
-// +build !aix,!darwin,!dragonfly,!freebsd,!illumos,!linux,!netbsd,!openbsd,!solaris,!plan9,!windows
-
-package filelock
-
-import "io/fs"
-
-type lockType int8
-
-const (
-	readLock = iota + 1
-	writeLock
-)
-
-func lock(f File, lt lockType) error {
-	return &fs.PathError{
-		Op:   lt.String(),
-		Path: f.Name(),
-		Err:  ErrNotSupported,
-	}
-}
-
-func unlock(f File) error {
-	return &fs.PathError{
-		Op:   "Unlock",
-		Path: f.Name(),
-		Err:  ErrNotSupported,
-	}
-}
-
-func isNotSupported(err error) bool {
-	return err == ErrNotSupported
-}
diff --git a/internal/lockedfile/internal/filelock/filelock_plan9.go b/internal/lockedfile/internal/filelock/filelock_plan9.go
deleted file mode 100644
index 908afb6..0000000
--- a/internal/lockedfile/internal/filelock/filelock_plan9.go
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright 2018 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.
-
-//go:build plan9
-// +build plan9
-
-package filelock
-
-import "io/fs"
-
-type lockType int8
-
-const (
-	readLock = iota + 1
-	writeLock
-)
-
-func lock(f File, lt lockType) error {
-	return &fs.PathError{
-		Op:   lt.String(),
-		Path: f.Name(),
-		Err:  ErrNotSupported,
-	}
-}
-
-func unlock(f File) error {
-	return &fs.PathError{
-		Op:   "Unlock",
-		Path: f.Name(),
-		Err:  ErrNotSupported,
-	}
-}
-
-func isNotSupported(err error) bool {
-	return err == ErrNotSupported
-}
diff --git a/internal/lockedfile/internal/filelock/filelock_test.go b/internal/lockedfile/internal/filelock/filelock_test.go
deleted file mode 100644
index 6c3f393..0000000
--- a/internal/lockedfile/internal/filelock/filelock_test.go
+++ /dev/null
@@ -1,209 +0,0 @@
-// Copyright 2018 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.
-
-//go:build unix || aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows
-// +build unix aix darwin dragonfly freebsd linux netbsd openbsd solaris windows
-
-package filelock_test
-
-import (
-	"fmt"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"runtime"
-	"testing"
-	"time"
-
-	"golang.org/x/tools/internal/lockedfile/internal/filelock"
-)
-
-func lock(t *testing.T, f *os.File) {
-	t.Helper()
-	err := filelock.Lock(f)
-	t.Logf("Lock(fd %d) = %v", f.Fd(), err)
-	if err != nil {
-		t.Fail()
-	}
-}
-
-func rLock(t *testing.T, f *os.File) {
-	t.Helper()
-	err := filelock.RLock(f)
-	t.Logf("RLock(fd %d) = %v", f.Fd(), err)
-	if err != nil {
-		t.Fail()
-	}
-}
-
-func unlock(t *testing.T, f *os.File) {
-	t.Helper()
-	err := filelock.Unlock(f)
-	t.Logf("Unlock(fd %d) = %v", f.Fd(), err)
-	if err != nil {
-		t.Fail()
-	}
-}
-
-func mustTempFile(t *testing.T) (f *os.File, remove func()) {
-	t.Helper()
-
-	base := filepath.Base(t.Name())
-	f, err := os.CreateTemp("", base)
-	if err != nil {
-		t.Fatalf(`os.CreateTemp("", %q) = %v`, base, err)
-	}
-	t.Logf("fd %d = %s", f.Fd(), f.Name())
-
-	return f, func() {
-		f.Close()
-		os.Remove(f.Name())
-	}
-}
-
-func mustOpen(t *testing.T, name string) *os.File {
-	t.Helper()
-
-	f, err := os.OpenFile(name, os.O_RDWR, 0)
-	if err != nil {
-		t.Fatalf("os.Open(%q) = %v", name, err)
-	}
-
-	t.Logf("fd %d = os.Open(%q)", f.Fd(), name)
-	return f
-}
-
-const (
-	quiescent            = 10 * time.Millisecond
-	probablyStillBlocked = 10 * time.Second
-)
-
-func mustBlock(t *testing.T, op string, f *os.File) (wait func(*testing.T)) {
-	t.Helper()
-
-	desc := fmt.Sprintf("%s(fd %d)", op, f.Fd())
-
-	done := make(chan struct{})
-	go func() {
-		t.Helper()
-		switch op {
-		case "Lock":
-			lock(t, f)
-		case "RLock":
-			rLock(t, f)
-		default:
-			panic("invalid op: " + op)
-		}
-		close(done)
-	}()
-
-	select {
-	case <-done:
-		t.Fatalf("%s unexpectedly did not block", desc)
-		return nil
-
-	case <-time.After(quiescent):
-		t.Logf("%s is blocked (as expected)", desc)
-		return func(t *testing.T) {
-			t.Helper()
-			select {
-			case <-time.After(probablyStillBlocked):
-				t.Fatalf("%s is unexpectedly still blocked", desc)
-			case <-done:
-			}
-		}
-	}
-}
-
-func TestLockExcludesLock(t *testing.T) {
-	t.Parallel()
-
-	f, remove := mustTempFile(t)
-	defer remove()
-
-	other := mustOpen(t, f.Name())
-	defer other.Close()
-
-	lock(t, f)
-	lockOther := mustBlock(t, "Lock", other)
-	unlock(t, f)
-	lockOther(t)
-	unlock(t, other)
-}
-
-func TestLockExcludesRLock(t *testing.T) {
-	t.Parallel()
-
-	f, remove := mustTempFile(t)
-	defer remove()
-
-	other := mustOpen(t, f.Name())
-	defer other.Close()
-
-	lock(t, f)
-	rLockOther := mustBlock(t, "RLock", other)
-	unlock(t, f)
-	rLockOther(t)
-	unlock(t, other)
-}
-
-func TestRLockExcludesOnlyLock(t *testing.T) {
-	t.Parallel()
-
-	f, remove := mustTempFile(t)
-	defer remove()
-	rLock(t, f)
-
-	f2 := mustOpen(t, f.Name())
-	defer f2.Close()
-
-	doUnlockTF := false
-	switch runtime.GOOS {
-	case "aix", "solaris":
-		// When using POSIX locks (as on Solaris), we can't safely read-lock the
-		// same inode through two different descriptors at the same time: when the
-		// first descriptor is closed, the second descriptor would still be open but
-		// silently unlocked. So a second RLock must block instead of proceeding.
-		lockF2 := mustBlock(t, "RLock", f2)
-		unlock(t, f)
-		lockF2(t)
-	default:
-		rLock(t, f2)
-		doUnlockTF = true
-	}
-
-	other := mustOpen(t, f.Name())
-	defer other.Close()
-	lockOther := mustBlock(t, "Lock", other)
-
-	unlock(t, f2)
-	if doUnlockTF {
-		unlock(t, f)
-	}
-	lockOther(t)
-	unlock(t, other)
-}
-
-func TestLockNotDroppedByExecCommand(t *testing.T) {
-	f, remove := mustTempFile(t)
-	defer remove()
-
-	lock(t, f)
-
-	other := mustOpen(t, f.Name())
-	defer other.Close()
-
-	// Some kinds of file locks are dropped when a duplicated or forked file
-	// descriptor is unlocked. Double-check that the approach used by os/exec does
-	// not accidentally drop locks.
-	cmd := exec.Command(os.Args[0], "-test.run=^$")
-	if err := cmd.Run(); err != nil {
-		t.Fatalf("exec failed: %v", err)
-	}
-
-	lockOther := mustBlock(t, "Lock", other)
-	unlock(t, f)
-	lockOther(t)
-	unlock(t, other)
-}
diff --git a/internal/lockedfile/internal/filelock/filelock_unix.go b/internal/lockedfile/internal/filelock/filelock_unix.go
deleted file mode 100644
index 878a1e7..0000000
--- a/internal/lockedfile/internal/filelock/filelock_unix.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2018 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.
-
-//go:build darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd
-// +build darwin dragonfly freebsd illumos linux netbsd openbsd
-
-package filelock
-
-import (
-	"io/fs"
-	"syscall"
-)
-
-type lockType int16
-
-const (
-	readLock  lockType = syscall.LOCK_SH
-	writeLock lockType = syscall.LOCK_EX
-)
-
-func lock(f File, lt lockType) (err error) {
-	for {
-		err = syscall.Flock(int(f.Fd()), int(lt))
-		if err != syscall.EINTR {
-			break
-		}
-	}
-	if err != nil {
-		return &fs.PathError{
-			Op:   lt.String(),
-			Path: f.Name(),
-			Err:  err,
-		}
-	}
-	return nil
-}
-
-func unlock(f File) error {
-	return lock(f, syscall.LOCK_UN)
-}
-
-func isNotSupported(err error) bool {
-	return err == syscall.ENOSYS || err == syscall.ENOTSUP || err == syscall.EOPNOTSUPP || err == ErrNotSupported
-}
diff --git a/internal/lockedfile/internal/filelock/filelock_windows.go b/internal/lockedfile/internal/filelock/filelock_windows.go
deleted file mode 100644
index 3273a81..0000000
--- a/internal/lockedfile/internal/filelock/filelock_windows.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2018 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.
-
-//go:build windows
-// +build windows
-
-package filelock
-
-import (
-	"io/fs"
-
-	"golang.org/x/sys/windows"
-)
-
-type lockType uint32
-
-const (
-	readLock  lockType = 0
-	writeLock lockType = windows.LOCKFILE_EXCLUSIVE_LOCK
-)
-
-const (
-	reserved = 0
-	allBytes = ^uint32(0)
-)
-
-func lock(f File, lt lockType) error {
-	// Per https://golang.org/issue/19098, “Programs currently expect the Fd
-	// method to return a handle that uses ordinary synchronous I/O.”
-	// However, LockFileEx still requires an OVERLAPPED structure,
-	// which contains the file offset of the beginning of the lock range.
-	// We want to lock the entire file, so we leave the offset as zero.
-	ol := new(windows.Overlapped)
-
-	err := windows.LockFileEx(windows.Handle(f.Fd()), uint32(lt), reserved, allBytes, allBytes, ol)
-	if err != nil {
-		return &fs.PathError{
-			Op:   lt.String(),
-			Path: f.Name(),
-			Err:  err,
-		}
-	}
-	return nil
-}
-
-func unlock(f File) error {
-	ol := new(windows.Overlapped)
-	err := windows.UnlockFileEx(windows.Handle(f.Fd()), reserved, allBytes, allBytes, ol)
-	if err != nil {
-		return &fs.PathError{
-			Op:   "Unlock",
-			Path: f.Name(),
-			Err:  err,
-		}
-	}
-	return nil
-}
-
-func isNotSupported(err error) bool {
-	switch err {
-	case windows.ERROR_NOT_SUPPORTED, windows.ERROR_CALL_NOT_IMPLEMENTED, ErrNotSupported:
-		return true
-	default:
-		return false
-	}
-}
diff --git a/internal/lockedfile/lockedfile.go b/internal/lockedfile/lockedfile.go
deleted file mode 100644
index 82e1a89..0000000
--- a/internal/lockedfile/lockedfile.go
+++ /dev/null
@@ -1,187 +0,0 @@
-// Copyright 2018 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 lockedfile creates and manipulates files whose contents should only
-// change atomically.
-package lockedfile
-
-import (
-	"fmt"
-	"io"
-	"io/fs"
-	"os"
-	"runtime"
-)
-
-// A File is a locked *os.File.
-//
-// Closing the file releases the lock.
-//
-// If the program exits while a file is locked, the operating system releases
-// the lock but may not do so promptly: callers must ensure that all locked
-// files are closed before exiting.
-type File struct {
-	osFile
-	closed bool
-}
-
-// osFile embeds a *os.File while keeping the pointer itself unexported.
-// (When we close a File, it must be the same file descriptor that we opened!)
-type osFile struct {
-	*os.File
-}
-
-// OpenFile is like os.OpenFile, but returns a locked file.
-// If flag includes os.O_WRONLY or os.O_RDWR, the file is write-locked;
-// otherwise, it is read-locked.
-func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) {
-	var (
-		f   = new(File)
-		err error
-	)
-	f.osFile.File, err = openFile(name, flag, perm)
-	if err != nil {
-		return nil, err
-	}
-
-	// Although the operating system will drop locks for open files when the go
-	// command exits, we want to hold locks for as little time as possible, and we
-	// especially don't want to leave a file locked after we're done with it. Our
-	// Close method is what releases the locks, so use a finalizer to report
-	// missing Close calls on a best-effort basis.
-	runtime.SetFinalizer(f, func(f *File) {
-		panic(fmt.Sprintf("lockedfile.File %s became unreachable without a call to Close", f.Name()))
-	})
-
-	return f, nil
-}
-
-// Open is like os.Open, but returns a read-locked file.
-func Open(name string) (*File, error) {
-	return OpenFile(name, os.O_RDONLY, 0)
-}
-
-// Create is like os.Create, but returns a write-locked file.
-func Create(name string) (*File, error) {
-	return OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
-}
-
-// Edit creates the named file with mode 0666 (before umask),
-// but does not truncate existing contents.
-//
-// If Edit succeeds, methods on the returned File can be used for I/O.
-// The associated file descriptor has mode O_RDWR and the file is write-locked.
-func Edit(name string) (*File, error) {
-	return OpenFile(name, os.O_RDWR|os.O_CREATE, 0666)
-}
-
-// Close unlocks and closes the underlying file.
-//
-// Close may be called multiple times; all calls after the first will return a
-// non-nil error.
-func (f *File) Close() error {
-	if f.closed {
-		return &fs.PathError{
-			Op:   "close",
-			Path: f.Name(),
-			Err:  fs.ErrClosed,
-		}
-	}
-	f.closed = true
-
-	err := closeFile(f.osFile.File)
-	runtime.SetFinalizer(f, nil)
-	return err
-}
-
-// Read opens the named file with a read-lock and returns its contents.
-func Read(name string) ([]byte, error) {
-	f, err := Open(name)
-	if err != nil {
-		return nil, err
-	}
-	defer f.Close()
-
-	return io.ReadAll(f)
-}
-
-// Write opens the named file (creating it with the given permissions if needed),
-// then write-locks it and overwrites it with the given content.
-func Write(name string, content io.Reader, perm fs.FileMode) (err error) {
-	f, err := OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
-	if err != nil {
-		return err
-	}
-
-	_, err = io.Copy(f, content)
-	if closeErr := f.Close(); err == nil {
-		err = closeErr
-	}
-	return err
-}
-
-// Transform invokes t with the result of reading the named file, with its lock
-// still held.
-//
-// If t returns a nil error, Transform then writes the returned contents back to
-// the file, making a best effort to preserve existing contents on error.
-//
-// t must not modify the slice passed to it.
-func Transform(name string, t func([]byte) ([]byte, error)) (err error) {
-	f, err := Edit(name)
-	if err != nil {
-		return err
-	}
-	defer f.Close()
-
-	old, err := io.ReadAll(f)
-	if err != nil {
-		return err
-	}
-
-	new, err := t(old)
-	if err != nil {
-		return err
-	}
-
-	if len(new) > len(old) {
-		// The overall file size is increasing, so write the tail first: if we're
-		// about to run out of space on the disk, we would rather detect that
-		// failure before we have overwritten the original contents.
-		if _, err := f.WriteAt(new[len(old):], int64(len(old))); err != nil {
-			// Make a best effort to remove the incomplete tail.
-			f.Truncate(int64(len(old)))
-			return err
-		}
-	}
-
-	// We're about to overwrite the old contents. In case of failure, make a best
-	// effort to roll back before we close the file.
-	defer func() {
-		if err != nil {
-			if _, err := f.WriteAt(old, 0); err == nil {
-				f.Truncate(int64(len(old)))
-			}
-		}
-	}()
-
-	if len(new) >= len(old) {
-		if _, err := f.WriteAt(new[:len(old)], 0); err != nil {
-			return err
-		}
-	} else {
-		if _, err := f.WriteAt(new, 0); err != nil {
-			return err
-		}
-		// The overall file size is decreasing, so shrink the file to its final size
-		// after writing. We do this after writing (instead of before) so that if
-		// the write fails, enough filesystem space will likely still be reserved
-		// to contain the previous contents.
-		if err := f.Truncate(int64(len(new))); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
diff --git a/internal/lockedfile/lockedfile_filelock.go b/internal/lockedfile/lockedfile_filelock.go
deleted file mode 100644
index 7c71672..0000000
--- a/internal/lockedfile/lockedfile_filelock.go
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright 2018 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.
-
-//go:build !plan9
-// +build !plan9
-
-package lockedfile
-
-import (
-	"io/fs"
-	"os"
-
-	"golang.org/x/tools/internal/lockedfile/internal/filelock"
-)
-
-func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
-	// On BSD systems, we could add the O_SHLOCK or O_EXLOCK flag to the OpenFile
-	// call instead of locking separately, but we have to support separate locking
-	// calls for Linux and Windows anyway, so it's simpler to use that approach
-	// consistently.
-
-	f, err := os.OpenFile(name, flag&^os.O_TRUNC, perm)
-	if err != nil {
-		return nil, err
-	}
-
-	switch flag & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR) {
-	case os.O_WRONLY, os.O_RDWR:
-		err = filelock.Lock(f)
-	default:
-		err = filelock.RLock(f)
-	}
-	if err != nil {
-		f.Close()
-		return nil, err
-	}
-
-	if flag&os.O_TRUNC == os.O_TRUNC {
-		if err := f.Truncate(0); err != nil {
-			// The documentation for os.O_TRUNC says “if possible, truncate file when
-			// opened”, but doesn't define “possible” (golang.org/issue/28699).
-			// We'll treat regular files (and symlinks to regular files) as “possible”
-			// and ignore errors for the rest.
-			if fi, statErr := f.Stat(); statErr != nil || fi.Mode().IsRegular() {
-				filelock.Unlock(f)
-				f.Close()
-				return nil, err
-			}
-		}
-	}
-
-	return f, nil
-}
-
-func closeFile(f *os.File) error {
-	// Since locking syscalls operate on file descriptors, we must unlock the file
-	// while the descriptor is still valid — that is, before the file is closed —
-	// and avoid unlocking files that are already closed.
-	err := filelock.Unlock(f)
-
-	if closeErr := f.Close(); err == nil {
-		err = closeErr
-	}
-	return err
-}
diff --git a/internal/lockedfile/lockedfile_plan9.go b/internal/lockedfile/lockedfile_plan9.go
deleted file mode 100644
index 40871e6..0000000
--- a/internal/lockedfile/lockedfile_plan9.go
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright 2018 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.
-
-//go:build plan9
-// +build plan9
-
-package lockedfile
-
-import (
-	"io/fs"
-	"math/rand"
-	"os"
-	"strings"
-	"time"
-)
-
-// Opening an exclusive-use file returns an error.
-// The expected error strings are:
-//
-//   - "open/create -- file is locked" (cwfs, kfs)
-//   - "exclusive lock" (fossil)
-//   - "exclusive use file already open" (ramfs)
-var lockedErrStrings = [...]string{
-	"file is locked",
-	"exclusive lock",
-	"exclusive use file already open",
-}
-
-// Even though plan9 doesn't support the Lock/RLock/Unlock functions to
-// manipulate already-open files, IsLocked is still meaningful: os.OpenFile
-// itself may return errors that indicate that a file with the ModeExclusive bit
-// set is already open.
-func isLocked(err error) bool {
-	s := err.Error()
-
-	for _, frag := range lockedErrStrings {
-		if strings.Contains(s, frag) {
-			return true
-		}
-	}
-
-	return false
-}
-
-func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
-	// Plan 9 uses a mode bit instead of explicit lock/unlock syscalls.
-	//
-	// Per http://man.cat-v.org/plan_9/5/stat: “Exclusive use files may be open
-	// for I/O by only one fid at a time across all clients of the server. If a
-	// second open is attempted, it draws an error.”
-	//
-	// So we can try to open a locked file, but if it fails we're on our own to
-	// figure out when it becomes available. We'll use exponential backoff with
-	// some jitter and an arbitrary limit of 500ms.
-
-	// If the file was unpacked or created by some other program, it might not
-	// have the ModeExclusive bit set. Set it before we call OpenFile, so that we
-	// can be confident that a successful OpenFile implies exclusive use.
-	if fi, err := os.Stat(name); err == nil {
-		if fi.Mode()&fs.ModeExclusive == 0 {
-			if err := os.Chmod(name, fi.Mode()|fs.ModeExclusive); err != nil {
-				return nil, err
-			}
-		}
-	} else if !os.IsNotExist(err) {
-		return nil, err
-	}
-
-	nextSleep := 1 * time.Millisecond
-	const maxSleep = 500 * time.Millisecond
-	for {
-		f, err := os.OpenFile(name, flag, perm|fs.ModeExclusive)
-		if err == nil {
-			return f, nil
-		}
-
-		if !isLocked(err) {
-			return nil, err
-		}
-
-		time.Sleep(nextSleep)
-
-		nextSleep += nextSleep
-		if nextSleep > maxSleep {
-			nextSleep = maxSleep
-		}
-		// Apply 10% jitter to avoid synchronizing collisions.
-		nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep))
-	}
-}
-
-func closeFile(f *os.File) error {
-	return f.Close()
-}
diff --git a/internal/lockedfile/lockedfile_test.go b/internal/lockedfile/lockedfile_test.go
deleted file mode 100644
index edf8851..0000000
--- a/internal/lockedfile/lockedfile_test.go
+++ /dev/null
@@ -1,268 +0,0 @@
-// Copyright 2018 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.
-
-//go:build unix || aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || plan9 || windows
-// +build unix aix darwin dragonfly freebsd linux netbsd openbsd solaris plan9 windows
-
-package lockedfile_test
-
-import (
-	"fmt"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"testing"
-	"time"
-
-	"golang.org/x/tools/internal/lockedfile"
-)
-
-func mustTempDir(t *testing.T) (dir string, remove func()) {
-	t.Helper()
-
-	dir, err := os.MkdirTemp("", filepath.Base(t.Name()))
-	if err != nil {
-		t.Fatal(err)
-	}
-	return dir, func() { os.RemoveAll(dir) }
-}
-
-const (
-	quiescent            = 10 * time.Millisecond
-	probablyStillBlocked = 10 * time.Second
-)
-
-func mustBlock(t *testing.T, desc string, f func()) (wait func(*testing.T)) {
-	t.Helper()
-
-	done := make(chan struct{})
-	go func() {
-		f()
-		close(done)
-	}()
-
-	select {
-	case <-done:
-		t.Fatalf("%s unexpectedly did not block", desc)
-		return nil
-
-	case <-time.After(quiescent):
-		return func(t *testing.T) {
-			t.Helper()
-			select {
-			case <-time.After(probablyStillBlocked):
-				t.Fatalf("%s is unexpectedly still blocked after %v", desc, probablyStillBlocked)
-			case <-done:
-			}
-		}
-	}
-}
-
-func TestMutexExcludes(t *testing.T) {
-	t.Parallel()
-
-	dir, remove := mustTempDir(t)
-	defer remove()
-
-	path := filepath.Join(dir, "lock")
-
-	mu := lockedfile.MutexAt(path)
-	t.Logf("mu := MutexAt(_)")
-
-	unlock, err := mu.Lock()
-	if err != nil {
-		t.Fatalf("mu.Lock: %v", err)
-	}
-	t.Logf("unlock, _  := mu.Lock()")
-
-	mu2 := lockedfile.MutexAt(mu.Path)
-	t.Logf("mu2 := MutexAt(mu.Path)")
-
-	wait := mustBlock(t, "mu2.Lock()", func() {
-		unlock2, err := mu2.Lock()
-		if err != nil {
-			t.Errorf("mu2.Lock: %v", err)
-			return
-		}
-		t.Logf("unlock2, _ := mu2.Lock()")
-		t.Logf("unlock2()")
-		unlock2()
-	})
-
-	t.Logf("unlock()")
-	unlock()
-	wait(t)
-}
-
-func TestReadWaitsForLock(t *testing.T) {
-	t.Parallel()
-
-	dir, remove := mustTempDir(t)
-	defer remove()
-
-	path := filepath.Join(dir, "timestamp.txt")
-
-	f, err := lockedfile.Create(path)
-	if err != nil {
-		t.Fatalf("Create: %v", err)
-	}
-	defer f.Close()
-
-	const (
-		part1 = "part 1\n"
-		part2 = "part 2\n"
-	)
-	_, err = f.WriteString(part1)
-	if err != nil {
-		t.Fatalf("WriteString: %v", err)
-	}
-	t.Logf("WriteString(%q) = <nil>", part1)
-
-	wait := mustBlock(t, "Read", func() {
-		b, err := lockedfile.Read(path)
-		if err != nil {
-			t.Errorf("Read: %v", err)
-			return
-		}
-
-		const want = part1 + part2
-		got := string(b)
-		if got == want {
-			t.Logf("Read(_) = %q", got)
-		} else {
-			t.Errorf("Read(_) = %q, _; want %q", got, want)
-		}
-	})
-
-	_, err = f.WriteString(part2)
-	if err != nil {
-		t.Errorf("WriteString: %v", err)
-	} else {
-		t.Logf("WriteString(%q) = <nil>", part2)
-	}
-	f.Close()
-
-	wait(t)
-}
-
-func TestCanLockExistingFile(t *testing.T) {
-	t.Parallel()
-
-	dir, remove := mustTempDir(t)
-	defer remove()
-	path := filepath.Join(dir, "existing.txt")
-
-	if err := os.WriteFile(path, []byte("ok"), 0777); err != nil {
-		t.Fatalf("os.WriteFile: %v", err)
-	}
-
-	f, err := lockedfile.Edit(path)
-	if err != nil {
-		t.Fatalf("first Edit: %v", err)
-	}
-
-	wait := mustBlock(t, "Edit", func() {
-		other, err := lockedfile.Edit(path)
-		if err != nil {
-			t.Errorf("second Edit: %v", err)
-		}
-		other.Close()
-	})
-
-	f.Close()
-	wait(t)
-}
-
-// TestSpuriousEDEADLK verifies that the spurious EDEADLK reported in
-// https://golang.org/issue/32817 no longer occurs.
-func TestSpuriousEDEADLK(t *testing.T) {
-	// 	P.1 locks file A.
-	// 	Q.3 locks file B.
-	// 	Q.3 blocks on file A.
-	// 	P.2 blocks on file B. (Spurious EDEADLK occurs here.)
-	// 	P.1 unlocks file A.
-	// 	Q.3 unblocks and locks file A.
-	// 	Q.3 unlocks files A and B.
-	// 	P.2 unblocks and locks file B.
-	// 	P.2 unlocks file B.
-
-	dirVar := t.Name() + "DIR"
-
-	if dir := os.Getenv(dirVar); dir != "" {
-		// Q.3 locks file B.
-		b, err := lockedfile.Edit(filepath.Join(dir, "B"))
-		if err != nil {
-			t.Fatal(err)
-		}
-		defer b.Close()
-
-		if err := os.WriteFile(filepath.Join(dir, "locked"), []byte("ok"), 0666); err != nil {
-			t.Fatal(err)
-		}
-
-		// Q.3 blocks on file A.
-		a, err := lockedfile.Edit(filepath.Join(dir, "A"))
-		// Q.3 unblocks and locks file A.
-		if err != nil {
-			t.Fatal(err)
-		}
-		defer a.Close()
-
-		// Q.3 unlocks files A and B.
-		return
-	}
-
-	dir, remove := mustTempDir(t)
-	defer remove()
-
-	// P.1 locks file A.
-	a, err := lockedfile.Edit(filepath.Join(dir, "A"))
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	cmd := exec.Command(os.Args[0], "-test.run="+t.Name())
-	cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", dirVar, dir))
-
-	qDone := make(chan struct{})
-	waitQ := mustBlock(t, "Edit A and B in subprocess", func() {
-		out, err := cmd.CombinedOutput()
-		if err != nil {
-			t.Errorf("%v:\n%s", err, out)
-		}
-		close(qDone)
-	})
-
-	// Wait until process Q has either failed or locked file B.
-	// Otherwise, P.2 might not block on file B as intended.
-locked:
-	for {
-		if _, err := os.Stat(filepath.Join(dir, "locked")); !os.IsNotExist(err) {
-			break locked
-		}
-		select {
-		case <-qDone:
-			break locked
-		case <-time.After(1 * time.Millisecond):
-		}
-	}
-
-	waitP2 := mustBlock(t, "Edit B", func() {
-		// P.2 blocks on file B. (Spurious EDEADLK occurs here.)
-		b, err := lockedfile.Edit(filepath.Join(dir, "B"))
-		// P.2 unblocks and locks file B.
-		if err != nil {
-			t.Error(err)
-			return
-		}
-		// P.2 unlocks file B.
-		b.Close()
-	})
-
-	// P.1 unlocks file A.
-	a.Close()
-
-	waitQ(t)
-	waitP2(t)
-}
diff --git a/internal/lockedfile/mutex.go b/internal/lockedfile/mutex.go
deleted file mode 100644
index 180a36c..0000000
--- a/internal/lockedfile/mutex.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2018 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 lockedfile
-
-import (
-	"fmt"
-	"os"
-	"sync"
-)
-
-// A Mutex provides mutual exclusion within and across processes by locking a
-// well-known file. Such a file generally guards some other part of the
-// filesystem: for example, a Mutex file in a directory might guard access to
-// the entire tree rooted in that directory.
-//
-// Mutex does not implement sync.Locker: unlike a sync.Mutex, a lockedfile.Mutex
-// can fail to lock (e.g. if there is a permission error in the filesystem).
-//
-// Like a sync.Mutex, a Mutex may be included as a field of a larger struct but
-// must not be copied after first use. The Path field must be set before first
-// use and must not be change thereafter.
-type Mutex struct {
-	Path string     // The path to the well-known lock file. Must be non-empty.
-	mu   sync.Mutex // A redundant mutex. The race detector doesn't know about file locking, so in tests we may need to lock something that it understands.
-}
-
-// MutexAt returns a new Mutex with Path set to the given non-empty path.
-func MutexAt(path string) *Mutex {
-	if path == "" {
-		panic("lockedfile.MutexAt: path must be non-empty")
-	}
-	return &Mutex{Path: path}
-}
-
-func (mu *Mutex) String() string {
-	return fmt.Sprintf("lockedfile.Mutex(%s)", mu.Path)
-}
-
-// Lock attempts to lock the Mutex.
-//
-// If successful, Lock returns a non-nil unlock function: it is provided as a
-// return-value instead of a separate method to remind the caller to check the
-// accompanying error. (See https://golang.org/issue/20803.)
-func (mu *Mutex) Lock() (unlock func(), err error) {
-	if mu.Path == "" {
-		panic("lockedfile.Mutex: missing Path during Lock")
-	}
-
-	// We could use either O_RDWR or O_WRONLY here. If we choose O_RDWR and the
-	// file at mu.Path is write-only, the call to OpenFile will fail with a
-	// permission error. That's actually what we want: if we add an RLock method
-	// in the future, it should call OpenFile with O_RDONLY and will require the
-	// files must be readable, so we should not let the caller make any
-	// assumptions about Mutex working with write-only files.
-	f, err := OpenFile(mu.Path, os.O_RDWR|os.O_CREATE, 0666)
-	if err != nil {
-		return nil, err
-	}
-	mu.mu.Lock()
-
-	return func() {
-		mu.mu.Unlock()
-		f.Close()
-	}, nil
-}
diff --git a/internal/lockedfile/transform_test.go b/internal/lockedfile/transform_test.go
deleted file mode 100644
index 2c0cd53..0000000
--- a/internal/lockedfile/transform_test.go
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright 2019 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.
-
-//go:build unix || aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || plan9 || windows
-// +build unix aix darwin dragonfly freebsd linux netbsd openbsd solaris plan9 windows
-
-package lockedfile_test
-
-import (
-	"bytes"
-	"encoding/binary"
-	"math/rand"
-	"path/filepath"
-	"testing"
-	"time"
-
-	"golang.org/x/tools/internal/lockedfile"
-)
-
-func isPowerOf2(x int) bool {
-	return x > 0 && x&(x-1) == 0
-}
-
-func roundDownToPowerOf2(x int) int {
-	if x <= 0 {
-		panic("nonpositive x")
-	}
-	bit := 1
-	for x != bit {
-		x = x &^ bit
-		bit <<= 1
-	}
-	return x
-}
-
-func TestTransform(t *testing.T) {
-	dir, remove := mustTempDir(t)
-	defer remove()
-	path := filepath.Join(dir, "blob.bin")
-
-	const maxChunkWords = 8 << 10
-	buf := make([]byte, 2*maxChunkWords*8)
-	for i := uint64(0); i < 2*maxChunkWords; i++ {
-		binary.LittleEndian.PutUint64(buf[i*8:], i)
-	}
-	if err := lockedfile.Write(path, bytes.NewReader(buf[:8]), 0666); err != nil {
-		t.Fatal(err)
-	}
-
-	var attempts int64 = 128
-	if !testing.Short() {
-		attempts *= 16
-	}
-	const parallel = 32
-
-	var sem = make(chan bool, parallel)
-
-	for n := attempts; n > 0; n-- {
-		sem <- true
-		go func() {
-			defer func() { <-sem }()
-
-			time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
-			chunkWords := roundDownToPowerOf2(rand.Intn(maxChunkWords) + 1)
-			offset := rand.Intn(chunkWords)
-
-			err := lockedfile.Transform(path, func(data []byte) (chunk []byte, err error) {
-				chunk = buf[offset*8 : (offset+chunkWords)*8]
-
-				if len(data)&^7 != len(data) {
-					t.Errorf("read %d bytes, but each write is an integer multiple of 8 bytes", len(data))
-					return chunk, nil
-				}
-
-				words := len(data) / 8
-				if !isPowerOf2(words) {
-					t.Errorf("read %d 8-byte words, but each write is a power-of-2 number of words", words)
-					return chunk, nil
-				}
-
-				u := binary.LittleEndian.Uint64(data)
-				for i := 1; i < words; i++ {
-					next := binary.LittleEndian.Uint64(data[i*8:])
-					if next != u+1 {
-						t.Errorf("wrote sequential integers, but read integer out of sequence at offset %d", i)
-						return chunk, nil
-					}
-					u = next
-				}
-
-				return chunk, nil
-			})
-
-			if err != nil {
-				t.Errorf("unexpected error from Transform: %v", err)
-			}
-		}()
-	}
-
-	for n := parallel; n > 0; n-- {
-		sem <- true
-	}
-}
diff --git a/internal/typesinternal/types.go b/internal/typesinternal/types.go
index 3c53fbc..ce7d435 100644
--- a/internal/typesinternal/types.go
+++ b/internal/typesinternal/types.go
@@ -11,8 +11,6 @@
 	"go/types"
 	"reflect"
 	"unsafe"
-
-	"golang.org/x/tools/go/types/objectpath"
 )
 
 func SetUsesCgo(conf *types.Config) bool {
@@ -52,10 +50,3 @@
 }
 
 var SetGoVersion = func(conf *types.Config, version string) bool { return false }
-
-// NewObjectpathEncoder returns a function closure equivalent to
-// objectpath.For but amortized for multiple (sequential) calls.
-// It is a temporary workaround, pending the approval of proposal 58668.
-//
-//go:linkname NewObjectpathFunc golang.org/x/tools/go/types/objectpath.newEncoderFor
-func NewObjectpathFunc() func(types.Object) (objectpath.Path, error)
diff --git a/present/doc.go b/present/doc.go
index 71f758f..2c88fb9 100644
--- a/present/doc.go
+++ b/present/doc.go
@@ -200,8 +200,11 @@
 a single marker character becomes a space and a doubled single
 marker quotes the marker character.
 
-Links can be included in any text with the form [[url][label]], or
-[[url]] to use the URL itself as the label.
+Links can be included in any text with either explicit labels
+or the URL itself as the label. For example:
+
+	[[url][label]]
+	[[url]]
 
 # Command Invocations