gopls/internal/regtest: eliminate DiagnosticAtRegexp

Replace DiagnosticAtRegexp with diagnostic filters. This allowed
eliminating DiagnosticExpectation, which was the only complicated
implementation of the Expectation interface. Replace Expectation with
the concrete (formerly named) SimpleExpectation.

Updates golang/go#39384

Change-Id: I6716e869609dce9777025557494c8f81a606e4ff
Reviewed-on: https://go-review.googlesource.com/c/tools/+/461939
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
diff --git a/gopls/internal/lsp/regtest/env.go b/gopls/internal/lsp/regtest/env.go
index 73e8ef3..ad60658 100644
--- a/gopls/internal/lsp/regtest/env.go
+++ b/gopls/internal/lsp/regtest/env.go
@@ -297,7 +297,7 @@
 		if v > finalVerdict {
 			finalVerdict = v
 		}
-		summary.WriteString(fmt.Sprintf("%v: %s\n", v, e.Description()))
+		summary.WriteString(fmt.Sprintf("%v: %s\n", v, e.Description))
 	}
 	return finalVerdict, summary.String()
 }
diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go
index 1c35925..d0896ff 100644
--- a/gopls/internal/lsp/regtest/expectation.go
+++ b/gopls/internal/lsp/regtest/expectation.go
@@ -14,17 +14,6 @@
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 )
 
-// An Expectation asserts that the state of the editor at a point in time
-// matches an expected condition. This is used for signaling in tests when
-// certain conditions in the editor are met.
-type Expectation interface {
-	// Check determines whether the state of the editor satisfies the
-	// expectation, returning the results that met the condition.
-	Check(State) Verdict
-	// Description is a human-readable description of the expectation.
-	Description() string
-}
-
 var (
 	// InitialWorkspaceLoad is an expectation that the workspace initial load has
 	// completed. It is verified via workdone reporting.
@@ -60,25 +49,15 @@
 	return fmt.Sprintf("unrecognized verdict %d", v)
 }
 
-// SimpleExpectation holds an arbitrary check func, and implements the Expectation interface.
-type SimpleExpectation struct {
-	check       func(State) Verdict
-	description string
-}
-
-// Check invokes e.check.
-func (e SimpleExpectation) Check(s State) Verdict {
-	return e.check(s)
-}
-
-// Description returns e.description.
-func (e SimpleExpectation) Description() string {
-	return e.description
+// Expectation holds an arbitrary check func, and implements the Expectation interface.
+type Expectation struct {
+	Check       func(State) Verdict
+	Description string
 }
 
 // OnceMet returns an Expectation that, once the precondition is met, asserts
 // that mustMeet is met.
-func OnceMet(precondition Expectation, mustMeets ...Expectation) *SimpleExpectation {
+func OnceMet(precondition Expectation, mustMeets ...Expectation) Expectation {
 	check := func(s State) Verdict {
 		switch pre := precondition.Check(s); pre {
 		case Unmeetable:
@@ -96,23 +75,23 @@
 		}
 	}
 	description := describeExpectations(mustMeets...)
-	return &SimpleExpectation{
-		check:       check,
-		description: fmt.Sprintf("once %q is met, must have:\n%s", precondition.Description(), description),
+	return Expectation{
+		Check:       check,
+		Description: fmt.Sprintf("once %q is met, must have:\n%s", precondition.Description, description),
 	}
 }
 
 func describeExpectations(expectations ...Expectation) string {
 	var descriptions []string
 	for _, e := range expectations {
-		descriptions = append(descriptions, e.Description())
+		descriptions = append(descriptions, e.Description)
 	}
 	return strings.Join(descriptions, "\n")
 }
 
 // AnyOf returns an expectation that is satisfied when any of the given
 // expectations is met.
-func AnyOf(anyOf ...Expectation) *SimpleExpectation {
+func AnyOf(anyOf ...Expectation) Expectation {
 	check := func(s State) Verdict {
 		for _, e := range anyOf {
 			verdict := e.Check(s)
@@ -123,9 +102,9 @@
 		return Unmet
 	}
 	description := describeExpectations(anyOf...)
-	return &SimpleExpectation{
-		check:       check,
-		description: fmt.Sprintf("Any of:\n%s", description),
+	return Expectation{
+		Check:       check,
+		Description: fmt.Sprintf("Any of:\n%s", description),
 	}
 }
 
@@ -139,7 +118,7 @@
 // why an expectation failed. This should allow us to significantly improve
 // test output: we won't need to summarize state at all, as the verdict
 // explanation itself should describe clearly why the expectation not met.
-func AllOf(allOf ...Expectation) *SimpleExpectation {
+func AllOf(allOf ...Expectation) Expectation {
 	check := func(s State) Verdict {
 		verdict := Met
 		for _, e := range allOf {
@@ -150,15 +129,15 @@
 		return verdict
 	}
 	description := describeExpectations(allOf...)
-	return &SimpleExpectation{
-		check:       check,
-		description: fmt.Sprintf("All of:\n%s", description),
+	return Expectation{
+		Check:       check,
+		Description: fmt.Sprintf("All of:\n%s", description),
 	}
 }
 
 // ReadDiagnostics is an 'expectation' that is used to read diagnostics
 // atomically. It is intended to be used with 'OnceMet'.
-func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) *SimpleExpectation {
+func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) Expectation {
 	check := func(s State) Verdict {
 		diags, ok := s.diagnostics[fileName]
 		if !ok {
@@ -167,29 +146,29 @@
 		*into = *diags
 		return Met
 	}
-	return &SimpleExpectation{
-		check:       check,
-		description: fmt.Sprintf("read diagnostics for %q", fileName),
+	return Expectation{
+		Check:       check,
+		Description: fmt.Sprintf("read diagnostics for %q", fileName),
 	}
 }
 
 // NoOutstandingWork asserts that there is no work initiated using the LSP
 // $/progress API that has not completed.
-func NoOutstandingWork() SimpleExpectation {
+func NoOutstandingWork() Expectation {
 	check := func(s State) Verdict {
 		if len(s.outstandingWork()) == 0 {
 			return Met
 		}
 		return Unmet
 	}
-	return SimpleExpectation{
-		check:       check,
-		description: "no outstanding work",
+	return Expectation{
+		Check:       check,
+		Description: "no outstanding work",
 	}
 }
 
 // NoShownMessage asserts that the editor has not received a ShowMessage.
-func NoShownMessage(subString string) SimpleExpectation {
+func NoShownMessage(subString string) Expectation {
 	check := func(s State) Verdict {
 		for _, m := range s.showMessage {
 			if strings.Contains(m.Message, subString) {
@@ -198,15 +177,15 @@
 		}
 		return Met
 	}
-	return SimpleExpectation{
-		check:       check,
-		description: fmt.Sprintf("no ShowMessage received containing %q", subString),
+	return Expectation{
+		Check:       check,
+		Description: fmt.Sprintf("no ShowMessage received containing %q", subString),
 	}
 }
 
 // ShownMessage asserts that the editor has received a ShowMessageRequest
 // containing the given substring.
-func ShownMessage(containing string) SimpleExpectation {
+func ShownMessage(containing string) Expectation {
 	check := func(s State) Verdict {
 		for _, m := range s.showMessage {
 			if strings.Contains(m.Message, containing) {
@@ -215,15 +194,15 @@
 		}
 		return Unmet
 	}
-	return SimpleExpectation{
-		check:       check,
-		description: "received ShowMessage",
+	return Expectation{
+		Check:       check,
+		Description: "received ShowMessage",
 	}
 }
 
 // ShowMessageRequest asserts that the editor has received a ShowMessageRequest
 // with an action item that has the given title.
-func ShowMessageRequest(title string) SimpleExpectation {
+func ShowMessageRequest(title string) Expectation {
 	check := func(s State) Verdict {
 		if len(s.showMessageRequest) == 0 {
 			return Unmet
@@ -238,9 +217,9 @@
 		}
 		return Unmet
 	}
-	return SimpleExpectation{
-		check:       check,
-		description: "received ShowMessageRequest",
+	return Expectation{
+		Check:       check,
+		Description: "received ShowMessageRequest",
 	}
 }
 
@@ -348,16 +327,16 @@
 // StartedWork expect a work item to have been started >= atLeast times.
 //
 // See CompletedWork.
-func StartedWork(title string, atLeast uint64) SimpleExpectation {
+func StartedWork(title string, atLeast uint64) Expectation {
 	check := func(s State) Verdict {
 		if s.startedWork()[title] >= atLeast {
 			return Met
 		}
 		return Unmet
 	}
-	return SimpleExpectation{
-		check:       check,
-		description: fmt.Sprintf("started work %q at least %d time(s)", title, atLeast),
+	return Expectation{
+		Check:       check,
+		Description: fmt.Sprintf("started work %q at least %d time(s)", title, atLeast),
 	}
 }
 
@@ -365,7 +344,7 @@
 //
 // Since the Progress API doesn't include any hidden metadata, we must use the
 // progress notification title to identify the work we expect to be completed.
-func CompletedWork(title string, count uint64, atLeast bool) SimpleExpectation {
+func CompletedWork(title string, count uint64, atLeast bool) Expectation {
 	check := func(s State) Verdict {
 		completed := s.completedWork()
 		if completed[title] == count || atLeast && completed[title] > count {
@@ -377,9 +356,9 @@
 	if atLeast {
 		desc = fmt.Sprintf("completed work %q at least %d time(s)", title, count)
 	}
-	return SimpleExpectation{
-		check:       check,
-		description: desc,
+	return Expectation{
+		Check:       check,
+		Description: desc,
 	}
 }
 
@@ -396,7 +375,7 @@
 //
 // If the token is not a progress token that the client has seen, this
 // expectation is Unmeetable.
-func CompletedProgress(token protocol.ProgressToken, into *WorkStatus) SimpleExpectation {
+func CompletedProgress(token protocol.ProgressToken, into *WorkStatus) Expectation {
 	check := func(s State) Verdict {
 		work, ok := s.work[token]
 		if !ok {
@@ -412,16 +391,16 @@
 		return Unmet
 	}
 	desc := fmt.Sprintf("completed work for token %v", token)
-	return SimpleExpectation{
-		check:       check,
-		description: desc,
+	return Expectation{
+		Check:       check,
+		Description: desc,
 	}
 }
 
 // OutstandingWork expects a work item to be outstanding. The given title must
 // be an exact match, whereas the given msg must only be contained in the work
 // item's message.
-func OutstandingWork(title, msg string) SimpleExpectation {
+func OutstandingWork(title, msg string) Expectation {
 	check := func(s State) Verdict {
 		for _, work := range s.work {
 			if work.complete {
@@ -433,32 +412,15 @@
 		}
 		return Unmet
 	}
-	return SimpleExpectation{
-		check:       check,
-		description: fmt.Sprintf("outstanding work: %q containing %q", title, msg),
+	return Expectation{
+		Check:       check,
+		Description: fmt.Sprintf("outstanding work: %q containing %q", title, msg),
 	}
 }
 
-// LogExpectation is an expectation on the log messages received by the editor
-// from gopls.
-type LogExpectation struct {
-	check       func([]*protocol.LogMessageParams) Verdict
-	description string
-}
-
-// Check implements the Expectation interface.
-func (e LogExpectation) Check(s State) Verdict {
-	return e.check(s.logs)
-}
-
-// Description implements the Expectation interface.
-func (e LogExpectation) Description() string {
-	return e.description
-}
-
 // NoErrorLogs asserts that the client has not received any log messages of
 // error severity.
-func NoErrorLogs() LogExpectation {
+func NoErrorLogs() Expectation {
 	return NoLogMatching(protocol.Error, "")
 }
 
@@ -468,14 +430,14 @@
 // The count argument specifies the expected number of matching logs. If
 // atLeast is set, this is a lower bound, otherwise there must be exactly cound
 // matching logs.
-func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) LogExpectation {
+func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) Expectation {
 	rec, err := regexp.Compile(re)
 	if err != nil {
 		panic(err)
 	}
-	check := func(msgs []*protocol.LogMessageParams) Verdict {
+	check := func(state State) Verdict {
 		var found int
-		for _, msg := range msgs {
+		for _, msg := range state.logs {
 			if msg.Type == typ && rec.Match([]byte(msg.Message)) {
 				found++
 			}
@@ -490,16 +452,16 @@
 	if atLeast {
 		desc = fmt.Sprintf("log message matching %q expected at least %v times", re, count)
 	}
-	return LogExpectation{
-		check:       check,
-		description: desc,
+	return Expectation{
+		Check:       check,
+		Description: desc,
 	}
 }
 
 // NoLogMatching asserts that the client has not received a log message
 // of type typ matching the regexp re. If re is an empty string, any log
 // message is considered a match.
-func NoLogMatching(typ protocol.MessageType, re string) LogExpectation {
+func NoLogMatching(typ protocol.MessageType, re string) Expectation {
 	var r *regexp.Regexp
 	if re != "" {
 		var err error
@@ -508,8 +470,8 @@
 			panic(err)
 		}
 	}
-	check := func(msgs []*protocol.LogMessageParams) Verdict {
-		for _, msg := range msgs {
+	check := func(state State) Verdict {
+		for _, msg := range state.logs {
 			if msg.Type != typ {
 				continue
 			}
@@ -519,25 +481,25 @@
 		}
 		return Met
 	}
-	return LogExpectation{
-		check:       check,
-		description: fmt.Sprintf("no log message matching %q", re),
+	return Expectation{
+		Check:       check,
+		Description: fmt.Sprintf("no log message matching %q", re),
 	}
 }
 
 // FileWatchMatching expects that a file registration matches re.
-func FileWatchMatching(re string) SimpleExpectation {
-	return SimpleExpectation{
-		check:       checkFileWatch(re, Met, Unmet),
-		description: fmt.Sprintf("file watch matching %q", re),
+func FileWatchMatching(re string) Expectation {
+	return Expectation{
+		Check:       checkFileWatch(re, Met, Unmet),
+		Description: fmt.Sprintf("file watch matching %q", re),
 	}
 }
 
 // NoFileWatchMatching expects that no file registration matches re.
-func NoFileWatchMatching(re string) SimpleExpectation {
-	return SimpleExpectation{
-		check:       checkFileWatch(re, Unmet, Met),
-		description: fmt.Sprintf("no file watch matching %q", re),
+func NoFileWatchMatching(re string) Expectation {
+	return Expectation{
+		Check:       checkFileWatch(re, Unmet, Met),
+		Description: fmt.Sprintf("no file watch matching %q", re),
 	}
 }
 
@@ -581,7 +543,7 @@
 // TODO(rfindley): remove this once TestWatchReplaceTargets has been revisited.
 //
 // Deprecated: use (No)FileWatchMatching
-func RegistrationMatching(re string) SimpleExpectation {
+func RegistrationMatching(re string) Expectation {
 	rec := regexp.MustCompile(re)
 	check := func(s State) Verdict {
 		for _, p := range s.registrations {
@@ -593,15 +555,15 @@
 		}
 		return Unmet
 	}
-	return SimpleExpectation{
-		check:       check,
-		description: fmt.Sprintf("registration matching %q", re),
+	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) SimpleExpectation {
+func UnregistrationMatching(re string) Expectation {
 	rec := regexp.MustCompile(re)
 	check := func(s State) Verdict {
 		for _, p := range s.unregistrations {
@@ -613,89 +575,12 @@
 		}
 		return Unmet
 	}
-	return SimpleExpectation{
-		check:       check,
-		description: fmt.Sprintf("unregistration matching %q", re),
+	return Expectation{
+		Check:       check,
+		Description: fmt.Sprintf("unregistration matching %q", re),
 	}
 }
 
-// A DiagnosticExpectation is a condition that must be met by the current set
-// of diagnostics for a file.
-type DiagnosticExpectation struct {
-	// optionally, the position of the diagnostic and the regex used to calculate it.
-	pos *protocol.Position
-	re  string
-
-	// optionally, the message that the diagnostic should contain.
-	message string
-
-	// whether the expectation is that the diagnostic is present, or absent.
-	present bool
-
-	// path is the scratch workdir-relative path to the file being asserted on.
-	path string
-
-	// optionally, the diagnostic source
-	source string
-}
-
-// Check implements the Expectation interface.
-func (e DiagnosticExpectation) Check(s State) Verdict {
-	diags, ok := s.diagnostics[e.path]
-	if !ok {
-		if !e.present {
-			return Met
-		}
-		return Unmet
-	}
-
-	found := false
-	for _, d := range diags.Diagnostics {
-		if e.pos != nil {
-			if d.Range.Start != *e.pos {
-				continue
-			}
-		}
-		if e.message != "" {
-			if !strings.Contains(d.Message, e.message) {
-				continue
-			}
-		}
-		if e.source != "" && e.source != d.Source {
-			continue
-		}
-		found = true
-		break
-	}
-
-	if found == e.present {
-		return Met
-	}
-	return Unmet
-}
-
-// Description implements the Expectation interface.
-func (e DiagnosticExpectation) Description() string {
-	desc := e.path + ":"
-	if !e.present {
-		desc += " no"
-	}
-	desc += " diagnostic"
-	if e.pos != nil {
-		desc += fmt.Sprintf(" at {line:%d, column:%d}", e.pos.Line, e.pos.Character)
-		if e.re != "" {
-			desc += fmt.Sprintf(" (location of %q)", e.re)
-		}
-	}
-	if e.message != "" {
-		desc += fmt.Sprintf(" with message %q", e.message)
-	}
-	if e.source != "" {
-		desc += fmt.Sprintf(" from source %q", e.source)
-	}
-	return desc
-}
-
 // Diagnostics asserts that there is at least one diagnostic matching the given
 // filters.
 func Diagnostics(filters ...DiagnosticFilter) Expectation {
@@ -722,9 +607,9 @@
 	for _, filter := range filters {
 		descs = append(descs, filter.desc)
 	}
-	return SimpleExpectation{
-		check:       check,
-		description: "any diagnostics " + strings.Join(descs, ", "),
+	return Expectation{
+		Check:       check,
+		Description: "any diagnostics " + strings.Join(descs, ", "),
 	}
 }
 
@@ -752,9 +637,9 @@
 	for _, filter := range filters {
 		descs = append(descs, filter.desc)
 	}
-	return SimpleExpectation{
-		check:       check,
-		description: "no diagnostics " + strings.Join(descs, ", "),
+	return Expectation{
+		Check:       check,
+		Description: "no diagnostics " + strings.Join(descs, ", "),
 	}
 }
 
@@ -839,14 +724,3 @@
 		},
 	}
 }
-
-// TODO(rfindley): eliminate all expectations below this point.
-
-// DiagnosticAtRegexp expects that there is a diagnostic entry at the start
-// position matching the regexp search string re in the buffer specified by
-// name. Note that this currently ignores the end position.
-func (e *Env) DiagnosticAtRegexp(name, re string) DiagnosticExpectation {
-	e.T.Helper()
-	pos := e.RegexpSearch(name, re)
-	return DiagnosticExpectation{path: name, pos: &pos, re: re, present: true}
-}
diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go
index c8674f0..2f9b550 100644
--- a/gopls/internal/regtest/completion/completion_test.go
+++ b/gopls/internal/regtest/completion/completion_test.go
@@ -319,7 +319,7 @@
 
 		// Await the diagnostics to add example.com/blah to the go.mod file.
 		env.Await(
-			env.DiagnosticAtRegexp("main.go", `"example.com/blah"`),
+			Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)),
 		)
 	})
 }
diff --git a/gopls/internal/regtest/diagnostics/analysis_test.go b/gopls/internal/regtest/diagnostics/analysis_test.go
index a038d15..308c25f 100644
--- a/gopls/internal/regtest/diagnostics/analysis_test.go
+++ b/gopls/internal/regtest/diagnostics/analysis_test.go
@@ -39,7 +39,7 @@
 
 		var d protocol.PublishDiagnosticsParams
 		env.AfterChange(
-			env.DiagnosticAtRegexp("main.go", "2006-02-01"),
+			Diagnostics(env.AtRegexp("main.go", "2006-02-01")),
 			ReadDiagnostics("main.go", &d),
 		)
 
diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go
index 1c11681..4ce509c 100644
--- a/gopls/internal/regtest/diagnostics/diagnostics_test.go
+++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go
@@ -49,7 +49,7 @@
 		env.OpenFile("main.go")
 		env.RegexpReplace("main.go", "Printl(n)", "")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("main.go", "Printl"),
+			Diagnostics(env.AtRegexp("main.go", "Printl")),
 			// Assert that this test has sent no error logs to the client. This is not
 			// strictly necessary for testing this regression, but is included here
 			// as an example of using the NoErrorLogs() expectation. Feel free to
@@ -74,7 +74,7 @@
 }
 `)
 		env.Await(
-			env.DiagnosticAtRegexp("main.go", "log"),
+			Diagnostics(env.AtRegexp("main.go", "log")),
 		)
 		env.SaveBuffer("main.go")
 		env.AfterChange(NoDiagnostics(ForFile("main.go")))
@@ -88,7 +88,7 @@
 `
 	Run(t, brokenFile, func(t *testing.T, env *Env) {
 		env.CreateBuffer("broken.go", brokenFile)
-		env.Await(env.DiagnosticAtRegexp("broken.go", "\"abc"))
+		env.Await(Diagnostics(env.AtRegexp("broken.go", "\"abc")))
 	})
 }
 
@@ -112,8 +112,8 @@
 	Run(t, badPackage, func(t *testing.T, env *Env) {
 		env.OpenFile("b.go")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("a.go", "a = 1"),
-			env.DiagnosticAtRegexp("b.go", "a = 2"),
+			Diagnostics(env.AtRegexp("a.go", "a = 1")),
+			Diagnostics(env.AtRegexp("b.go", "a = 2")),
 		)
 
 		// Fix the error by editing the const name in b.go to `b`.
@@ -128,7 +128,10 @@
 func TestDiagnosticClearingOnDelete_Issue37049(t *testing.T) {
 	Run(t, badPackage, func(t *testing.T, env *Env) {
 		env.OpenFile("a.go")
-		env.Await(env.DiagnosticAtRegexp("a.go", "a = 1"), env.DiagnosticAtRegexp("b.go", "a = 2"))
+		env.Await(
+			Diagnostics(env.AtRegexp("a.go", "a = 1")),
+			Diagnostics(env.AtRegexp("b.go", "a = 2")),
+		)
 		env.RemoveWorkspaceFile("b.go")
 
 		env.Await(
@@ -144,14 +147,14 @@
 
 const a = 3`)
 		env.AfterChange(
-			env.DiagnosticAtRegexp("a.go", "a = 1"),
-			env.DiagnosticAtRegexp("b.go", "a = 2"),
-			env.DiagnosticAtRegexp("c.go", "a = 3"),
+			Diagnostics(env.AtRegexp("a.go", "a = 1")),
+			Diagnostics(env.AtRegexp("b.go", "a = 2")),
+			Diagnostics(env.AtRegexp("c.go", "a = 3")),
 		)
 		env.CloseBuffer("c.go")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("a.go", "a = 1"),
-			env.DiagnosticAtRegexp("b.go", "a = 2"),
+			Diagnostics(env.AtRegexp("a.go", "a = 1")),
+			Diagnostics(env.AtRegexp("b.go", "a = 2")),
 			NoDiagnostics(ForFile("c.go")),
 		)
 	})
@@ -171,7 +174,7 @@
 `,
 		})
 		env.AfterChange(
-			env.DiagnosticAtRegexp("c/c.go", "http.MethodGet"),
+			Diagnostics(env.AtRegexp("c/c.go", "http.MethodGet")),
 		)
 		// Save file, which will organize imports, adding the expected import.
 		// Expect the diagnostics to clear.
@@ -211,7 +214,7 @@
 // not break the workspace.
 func TestDeleteTestVariant(t *testing.T) {
 	Run(t, test38878, func(t *testing.T, env *Env) {
-		env.Await(env.DiagnosticAtRegexp("a_test.go", `f\((3)\)`))
+		env.Await(Diagnostics(env.AtRegexp("a_test.go", `f\((3)\)`)))
 		env.RemoveWorkspaceFile("a_test.go")
 		env.AfterChange(NoDiagnostics(ForFile("a_test.go")))
 
@@ -219,7 +222,7 @@
 		// triggering a metadata load.
 		env.OpenFile("a.go")
 		env.RegexpReplace("a.go", `// import`, "import")
-		env.AfterChange(env.DiagnosticAtRegexp("a.go", `"fmt"`))
+		env.AfterChange(Diagnostics(env.AtRegexp("a.go", `"fmt"`)))
 	})
 }
 
@@ -258,7 +261,7 @@
 		Run(t, noMod, func(t *testing.T, env *Env) {
 			env.OnceMet(
 				InitialWorkspaceLoad,
-				env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`),
+				Diagnostics(env.AtRegexp("main.go", `"mod.com/bob"`)),
 			)
 			env.CreateBuffer("go.mod", `module mod.com
 
@@ -268,7 +271,7 @@
 			var d protocol.PublishDiagnosticsParams
 			env.AfterChange(
 				NoDiagnostics(ForFile("main.go")),
-				env.DiagnosticAtRegexp("bob/bob.go", "x"),
+				Diagnostics(env.AtRegexp("bob/bob.go", "x")),
 				ReadDiagnostics("bob/bob.go", &d),
 			)
 			if len(d.Diagnostics) != 1 {
@@ -279,12 +282,12 @@
 	t.Run("initialized", func(t *testing.T) {
 		Run(t, noMod, func(t *testing.T, env *Env) {
 			env.Await(
-				env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`),
+				Diagnostics(env.AtRegexp("main.go", `"mod.com/bob"`)),
 			)
 			env.RunGoCommand("mod", "init", "mod.com")
 			env.AfterChange(
 				NoDiagnostics(ForFile("main.go")),
-				env.DiagnosticAtRegexp("bob/bob.go", "x"),
+				Diagnostics(env.AtRegexp("bob/bob.go", "x")),
 			)
 		})
 	})
@@ -294,14 +297,14 @@
 			Modes(Default),
 		).Run(t, noMod, func(t *testing.T, env *Env) {
 			env.Await(
-				env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`),
+				Diagnostics(env.AtRegexp("main.go", `"mod.com/bob"`)),
 			)
 			if err := env.Sandbox.RunGoCommand(env.Ctx, "", "mod", []string{"init", "mod.com"}, true); err != nil {
 				t.Fatal(err)
 			}
 			env.AfterChange(
 				NoDiagnostics(ForFile("main.go")),
-				env.DiagnosticAtRegexp("bob/bob.go", "x"),
+				Diagnostics(env.AtRegexp("bob/bob.go", "x")),
 			)
 		})
 	})
@@ -349,7 +352,7 @@
 		env.OpenFile("lib.go")
 		env.RegexpReplace("lib.go", "_ = x", "var y int")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("lib.go", "y int"),
+			Diagnostics(env.AtRegexp("lib.go", "y int")),
 			NoDiagnostics(ForFile("lib_test.go")),
 		)
 	})
@@ -418,7 +421,7 @@
 		// Check that gopackages correctly loaded this dependency. We should get a
 		// diagnostic for the wrong formatting type.
 		// TODO: we should be able to easily also match the diagnostic message.
-		env.Await(env.DiagnosticAtRegexp("print.go", "fmt.Printf"))
+		env.Await(Diagnostics(env.AtRegexp("print.go", "fmt.Printf")))
 	})
 }
 
@@ -441,7 +444,7 @@
 `
 	Run(t, adHoc, func(t *testing.T, env *Env) {
 		env.OpenFile("b/b.go")
-		env.Await(env.DiagnosticAtRegexp("b/b.go", "x"))
+		env.Await(Diagnostics(env.AtRegexp("b/b.go", "x")))
 	})
 }
 
@@ -462,7 +465,7 @@
 		},
 	).Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
-		env.AfterChange(env.DiagnosticAtRegexp("main.go", "fmt"))
+		env.AfterChange(Diagnostics(env.AtRegexp("main.go", "fmt")))
 		env.SaveBuffer("main.go")
 		env.AfterChange(NoDiagnostics(ForFile("main.go")))
 	})
@@ -602,7 +605,7 @@
 		if testenv.Go1Point() >= 16 {
 			env.RegexpReplace("x/x.go", `package x`, `package main`)
 			env.AfterChange(
-				env.DiagnosticAtRegexp("x/main.go", `fmt`),
+				Diagnostics(env.AtRegexp("x/main.go", `fmt`)),
 			)
 		}
 	})
@@ -644,7 +647,7 @@
 		env.OpenFile("main.go")
 		var d protocol.PublishDiagnosticsParams
 		env.AfterChange(
-			env.DiagnosticAtRegexp("main.go", `"github.com/ardanlabs/conf"`),
+			Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`)),
 			ReadDiagnostics("main.go", &d),
 		)
 		env.ApplyQuickFixes("main.go", d.Diagnostics)
@@ -656,7 +659,7 @@
 		// diagnostic and a fix to remove the import.
 		env.RegexpReplace("main.go", "_ = conf.ErrHelpWanted", "//_ = conf.ErrHelpWanted")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("main.go", `"github.com/ardanlabs/conf"`),
+			Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`)),
 		)
 		env.SaveBuffer("main.go")
 		// Expect a diagnostic and fix to remove the dependency in the go.mod.
@@ -674,7 +677,7 @@
 		env.RegexpReplace("main.go", "//_ = conf.ErrHelpWanted", "_ = conf.ErrHelpWanted")
 		env.SaveBuffer("main.go")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("main.go", `"github.com/ardanlabs/conf"`),
+			Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`)),
 		)
 	})
 }
@@ -772,8 +775,8 @@
 		// There are errors in the code to ensure all is working as expected.
 		env.OpenFile("hello/hello.go")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("hello/hello.go", "x"),
-			env.DiagnosticAtRegexp("hello/hello_test.go", "x"),
+			Diagnostics(env.AtRegexp("hello/hello.go", "x")),
+			Diagnostics(env.AtRegexp("hello/hello_test.go", "x")),
 		)
 
 		// Create an empty file with the intention of making it an x test.
@@ -801,7 +804,7 @@
 		// Expect a diagnostic for the missing import. Save, which should
 		// trigger import organization. The diagnostic should clear.
 		env.AfterChange(
-			env.DiagnosticAtRegexp("hello/hello_x_test.go", "hello.Hello"),
+			Diagnostics(env.AtRegexp("hello/hello_x_test.go", "hello.Hello")),
 		)
 		env.SaveBuffer("hello/hello_x_test.go")
 		env.AfterChange(
@@ -834,7 +837,7 @@
 }
 `)
 		env.Await(
-			env.DiagnosticAtRegexp("foo/bar_test.go", "x"),
+			Diagnostics(env.AtRegexp("foo/bar_test.go", "x")),
 		)
 	})
 }
@@ -915,7 +918,7 @@
 
 		// We should still get diagnostics for files that exist.
 		env.RegexpReplace("b/b.go", `a.A`, "a.Nonexistant")
-		env.Await(env.DiagnosticAtRegexp("b/b.go", `Nonexistant`))
+		env.Await(Diagnostics(env.AtRegexp("b/b.go", `Nonexistant`)))
 	})
 }
 
@@ -961,7 +964,7 @@
 		Run(t, mod, func(t *testing.T, env *Env) {
 			writeGoVim(env, "p/p.go", p)
 			writeGoVim(env, "main.go", main)
-			env.Await(env.DiagnosticAtRegexp("main.go", "5"))
+			env.Await(Diagnostics(env.AtRegexp("main.go", "5")))
 		})
 	})
 
@@ -991,9 +994,9 @@
 }
 `)
 			env.Await(
-				env.DiagnosticAtRegexp("main.go", "5"),
-				env.DiagnosticAtRegexp("p/p_test.go", "5"),
-				env.DiagnosticAtRegexp("p/x_test.go", "5"),
+				Diagnostics(env.AtRegexp("main.go", "5")),
+				Diagnostics(env.AtRegexp("p/p_test.go", "5")),
+				Diagnostics(env.AtRegexp("p/x_test.go", "5")),
 			)
 			env.RegexpReplace("p/p.go", "s string", "i int")
 			env.AfterChange(
@@ -1024,7 +1027,7 @@
 	).Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("a/a.go")
 		env.Await(
-			env.DiagnosticAtRegexp("a/a.go", "x"),
+			Diagnostics(env.AtRegexp("a/a.go", "x")),
 		)
 	})
 }
@@ -1116,7 +1119,7 @@
 }
 `))
 		env.Await(
-			env.DiagnosticAtRegexp("main.go", "x"),
+			Diagnostics(env.AtRegexp("main.go", "x")),
 		)
 	})
 }
@@ -1164,7 +1167,7 @@
 	).Run(t, mod, func(t *testing.T, env *Env) {
 		env.OpenFile("a/main.go")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("main.go", "x"),
+			Diagnostics(env.AtRegexp("main.go", "x")),
 		)
 	})
 	WithOptions(
@@ -1282,7 +1285,7 @@
 	Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("a/a.go")
 		env.Await(
-			env.DiagnosticAtRegexp("a/a.go", "x"),
+			Diagnostics(env.AtRegexp("a/a.go", "x")),
 		)
 		env.OpenFile("a/a_exclude.go")
 		env.Await(
@@ -1497,7 +1500,7 @@
 		)
 		env.RemoveWorkspaceFile("bob")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("cmd/main.go", `"mod.com/bob"`),
+			Diagnostics(env.AtRegexp("cmd/main.go", `"mod.com/bob"`)),
 			NoDiagnostics(ForFile("bob/bob.go")),
 			NoFileWatchMatching("bob"),
 		)
@@ -1675,7 +1678,7 @@
 		// Expect a diagnostic in a nested module.
 		env.OpenFile("nested/hello/hello.go")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("nested/hello/hello.go", "helloHelper"),
+			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"),
 		)
@@ -1716,7 +1719,7 @@
 
 	Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("foo.go")
-		env.AfterChange(env.DiagnosticAtRegexp("bar.go", `Foo`))
+		env.AfterChange(Diagnostics(env.AtRegexp("bar.go", `Foo`)))
 		env.RegexpReplace("foo.go", `\+build`, "")
 		env.AfterChange(NoDiagnostics(ForFile("bar.go")))
 	})
@@ -1747,14 +1750,14 @@
 		env.OpenFile("main.go")
 		env.OpenFile("other.go")
 		env.Await(
-			env.DiagnosticAtRegexp("main.go", "asdf"),
-			env.DiagnosticAtRegexp("main.go", "fdas"),
+			Diagnostics(env.AtRegexp("main.go", "asdf")),
+			Diagnostics(env.AtRegexp("main.go", "fdas")),
 		)
 		env.SetBufferContent("other.go", "package main\n\nasdf")
 		// The new diagnostic in other.go should not suppress diagnostics in main.go.
 		env.AfterChange(
 			Diagnostics(env.AtRegexp("other.go", "asdf"), WithMessage("expected declaration")),
-			env.DiagnosticAtRegexp("main.go", "asdf"),
+			Diagnostics(env.AtRegexp("main.go", "asdf")),
 		)
 	})
 }
@@ -1842,7 +1845,7 @@
 		var d protocol.PublishDiagnosticsParams
 		env.OnceMet(
 			InitialWorkspaceLoad,
-			env.DiagnosticAtRegexp("main.go", `C`),
+			Diagnostics(env.AtRegexp("main.go", `C`)),
 			ReadDiagnostics("main.go", &d),
 		)
 		if fixes := env.GetQuickFixes("main.go", d.Diagnostics); len(fixes) != 0 {
diff --git a/gopls/internal/regtest/diagnostics/undeclared_test.go b/gopls/internal/regtest/diagnostics/undeclared_test.go
index bba72c4..a6289c9 100644
--- a/gopls/internal/regtest/diagnostics/undeclared_test.go
+++ b/gopls/internal/regtest/diagnostics/undeclared_test.go
@@ -46,7 +46,7 @@
 		env.OpenFile("a/a.go")
 		var adiags protocol.PublishDiagnosticsParams
 		env.AfterChange(
-			env.DiagnosticAtRegexp("a/a.go", "x"),
+			Diagnostics(env.AtRegexp("a/a.go", "x")),
 			ReadDiagnostics("a/a.go", &adiags),
 		)
 		env.Await()
@@ -61,7 +61,7 @@
 		env.OpenFile("b/b.go")
 		var bdiags protocol.PublishDiagnosticsParams
 		env.AfterChange(
-			env.DiagnosticAtRegexp("b/b.go", "y = y"),
+			Diagnostics(env.AtRegexp("b/b.go", "y = y")),
 			ReadDiagnostics("b/b.go", &bdiags),
 		)
 		if got := len(bdiags.Diagnostics); got != 1 {
diff --git a/gopls/internal/regtest/misc/generate_test.go b/gopls/internal/regtest/misc/generate_test.go
index 9fd448c..69a28e2 100644
--- a/gopls/internal/regtest/misc/generate_test.go
+++ b/gopls/internal/regtest/misc/generate_test.go
@@ -60,7 +60,7 @@
 
 	Run(t, generatedWorkspace, func(t *testing.T, env *Env) {
 		env.Await(
-			env.DiagnosticAtRegexp("main.go", "lib1.(Answer)"),
+			Diagnostics(env.AtRegexp("main.go", "lib1.(Answer)")),
 		)
 		env.RunGenerate("./lib1")
 		env.RunGenerate("./lib2")
diff --git a/gopls/internal/regtest/misc/imports_test.go b/gopls/internal/regtest/misc/imports_test.go
index b9804d0..513171a 100644
--- a/gopls/internal/regtest/misc/imports_test.go
+++ b/gopls/internal/regtest/misc/imports_test.go
@@ -156,7 +156,7 @@
 		ProxyFiles(proxy),
 	).Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
-		env.AfterChange(env.DiagnosticAtRegexp("main.go", `y.Y`))
+		env.AfterChange(Diagnostics(env.AtRegexp("main.go", `y.Y`)))
 		env.SaveBuffer("main.go")
 		env.AfterChange(NoDiagnostics(ForFile("main.go")))
 		path, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `y.(Y)`))
@@ -200,7 +200,7 @@
 		env.OpenFile("a/a.go")
 		var d protocol.PublishDiagnosticsParams
 		env.AfterChange(
-			env.DiagnosticAtRegexp("a/a.go", "os.Stat"),
+			Diagnostics(env.AtRegexp("a/a.go", "os.Stat")),
 			ReadDiagnostics("a/a.go", &d),
 		)
 		env.ApplyQuickFixes("a/a.go", d.Diagnostics)
@@ -247,7 +247,7 @@
 `
 	Run(t, pkg, func(t *testing.T, env *Env) {
 		env.OpenFile("caller/caller.go")
-		env.AfterChange(env.DiagnosticAtRegexp("caller/caller.go", "a.Test"))
+		env.AfterChange(Diagnostics(env.AtRegexp("caller/caller.go", "a.Test")))
 
 		// Saving caller.go should trigger goimports, which should find a.Test in
 		// the mod.com module, thanks to the go.work file.
diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go
index 81b3de8..86fd567 100644
--- a/gopls/internal/regtest/misc/rename_test.go
+++ b/gopls/internal/regtest/misc/rename_test.go
@@ -405,15 +405,15 @@
 		// Initially, we should have diagnostics on both X's, for their duplicate declaration.
 		env.OnceMet(
 			InitialWorkspaceLoad,
-			env.DiagnosticAtRegexp("a/a.go", "X"),
-			env.DiagnosticAtRegexp("a/x.go", "X"),
+			Diagnostics(env.AtRegexp("a/a.go", "X")),
+			Diagnostics(env.AtRegexp("a/x.go", "X")),
 		)
 
 		// Moving x.go should make the diagnostic go away.
 		env.RenameFile("a/x.go", "b/x.go")
 		env.AfterChange(
-			NoDiagnostics(ForFile("a/a.go")),            // no more duplicate declarations
-			env.DiagnosticAtRegexp("b/b.go", "package"), // as package names mismatch
+			NoDiagnostics(ForFile("a/a.go")),               // no more duplicate declarations
+			Diagnostics(env.AtRegexp("b/b.go", "package")), // as package names mismatch
 		)
 
 		// Renaming should also work on open buffers.
@@ -422,15 +422,15 @@
 		// Moving x.go back to a/ should cause the diagnostics to reappear.
 		env.RenameFile("b/x.go", "a/x.go")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("a/a.go", "X"),
-			env.DiagnosticAtRegexp("a/x.go", "X"),
+			Diagnostics(env.AtRegexp("a/a.go", "X")),
+			Diagnostics(env.AtRegexp("a/x.go", "X")),
 		)
 
 		// Renaming the entire directory should move both the open and closed file.
 		env.RenameFile("a", "x")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("x/a.go", "X"),
-			env.DiagnosticAtRegexp("x/x.go", "X"),
+			Diagnostics(env.AtRegexp("x/a.go", "X")),
+			Diagnostics(env.AtRegexp("x/x.go", "X")),
 		)
 
 		// As a sanity check, verify that x/x.go is open.
diff --git a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go
index 422147c..e083faa 100644
--- a/gopls/internal/regtest/misc/semantictokens_test.go
+++ b/gopls/internal/regtest/misc/semantictokens_test.go
@@ -93,7 +93,9 @@
 		Settings{"semanticTokens": true},
 	).Run(t, src, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
-		env.AfterChange(env.DiagnosticAtRegexp("main.go", "for range"))
+		env.AfterChange(
+			Diagnostics(env.AtRegexp("main.go", "for range")),
+		)
 		p := &protocol.SemanticTokensParams{
 			TextDocument: protocol.TextDocumentIdentifier{
 				URI: env.Sandbox.Workdir.URI("main.go"),
diff --git a/gopls/internal/regtest/misc/shared_test.go b/gopls/internal/regtest/misc/shared_test.go
index 84eb99f..de44167 100644
--- a/gopls/internal/regtest/misc/shared_test.go
+++ b/gopls/internal/regtest/misc/shared_test.go
@@ -54,8 +54,8 @@
 		env2.RegexpReplace("main.go", "\\)\n(})", "")
 
 		// Now check that we got different diagnostics in each environment.
-		env1.Await(env1.DiagnosticAtRegexp("main.go", "Printl"))
-		env2.Await(env2.DiagnosticAtRegexp("main.go", "$"))
+		env1.Await(Diagnostics(env1.AtRegexp("main.go", "Printl")))
+		env2.Await(Diagnostics(env2.AtRegexp("main.go", "$")))
 
 		// Now close editor #2, and verify that operation in editor #1 is
 		// unaffected.
diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go
index b34ddfc..9d73272 100644
--- a/gopls/internal/regtest/misc/vuln_test.go
+++ b/gopls/internal/regtest/misc/vuln_test.go
@@ -485,7 +485,7 @@
 
 		gotDiagnostics := &protocol.PublishDiagnosticsParams{}
 		env.AfterChange(
-			env.DiagnosticAtRegexp("go.mod", `golang.org/amod`),
+			Diagnostics(env.AtRegexp("go.mod", `golang.org/amod`)),
 			ReadDiagnostics("go.mod", gotDiagnostics),
 		)
 
@@ -589,7 +589,7 @@
 						ShownMessage("Found"),
 					)
 					env.OnceMet(
-						env.DiagnosticAtRegexp("go.mod", "golang.org/bmod"),
+						Diagnostics(env.AtRegexp("go.mod", "golang.org/bmod")),
 						ReadDiagnostics("go.mod", gotDiagnostics),
 					)
 					// We expect only one diagnostic for GO-2022-02.
@@ -636,7 +636,7 @@
 		)
 		// Vulncheck diagnostics asynchronous to the vulncheck command.
 		env.OnceMet(
-			env.DiagnosticAtRegexp("go.mod", `golang.org/amod`),
+			Diagnostics(env.AtRegexp("go.mod", `golang.org/amod`)),
 			ReadDiagnostics("go.mod", gotDiagnostics),
 		)
 
@@ -803,7 +803,7 @@
 
 		// Vulncheck diagnostics asynchronous to the vulncheck command.
 		env.OnceMet(
-			env.DiagnosticAtRegexp("go.mod", "golang.org/bmod"),
+			Diagnostics(env.AtRegexp("go.mod", "golang.org/bmod")),
 			ReadDiagnostics("go.mod", gotDiagnostics),
 		)
 
diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go
index e88368a..d3df215 100644
--- a/gopls/internal/regtest/modfile/modfile_test.go
+++ b/gopls/internal/regtest/modfile/modfile_test.go
@@ -93,7 +93,7 @@
 			goModContent := env.ReadWorkspaceFile("a/go.mod")
 			env.OpenFile("a/main.go")
 			env.AfterChange(
-				env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""),
+				Diagnostics(env.AtRegexp("a/main.go", "\"example.com/blah\"")),
 			)
 			if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent {
 				t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got))
@@ -102,7 +102,7 @@
 			// Confirm that the go.mod file still does not change.
 			env.SaveBuffer("a/main.go")
 			env.Await(
-				env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""),
+				Diagnostics(env.AtRegexp("a/main.go", "\"example.com/blah\"")),
 			)
 			if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent {
 				t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got))
@@ -143,7 +143,7 @@
 
 			env.WriteWorkspaceFile("a/main.go", mainContent)
 			env.AfterChange(
-				env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""),
+				Diagnostics(env.AtRegexp("a/main.go", "\"example.com/blah\"")),
 			)
 			if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent {
 				t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got))
@@ -184,7 +184,7 @@
 		env.OpenFile("a/main.go")
 		var d protocol.PublishDiagnosticsParams
 		env.AfterChange(
-			env.DiagnosticAtRegexp("a/main.go", `"example.com/blah"`),
+			Diagnostics(env.AtRegexp("a/main.go", `"example.com/blah"`)),
 			ReadDiagnostics("a/main.go", &d),
 		)
 		var goGetDiag protocol.Diagnostic
@@ -231,7 +231,7 @@
 		env.OpenFile("a/main.go")
 		var d protocol.PublishDiagnosticsParams
 		env.AfterChange(
-			env.DiagnosticAtRegexp("a/main.go", `"random.org/blah"`),
+			Diagnostics(env.AtRegexp("a/main.go", `"random.org/blah"`)),
 			ReadDiagnostics("a/main.go", &d),
 		)
 		var randomDiag protocol.Diagnostic
@@ -285,7 +285,7 @@
 		env.OpenFile("a/main.go")
 		var d protocol.PublishDiagnosticsParams
 		env.AfterChange(
-			env.DiagnosticAtRegexp("a/main.go", `"random.org/blah"`),
+			Diagnostics(env.AtRegexp("a/main.go", `"random.org/blah"`)),
 			ReadDiagnostics("a/main.go", &d),
 		)
 		var randomDiag protocol.Diagnostic
@@ -334,7 +334,7 @@
 		env.OpenFile("a/go.mod")
 		var d protocol.PublishDiagnosticsParams
 		env.AfterChange(
-			env.DiagnosticAtRegexp("a/go.mod", "// indirect"),
+			Diagnostics(env.AtRegexp("a/go.mod", "// indirect")),
 			ReadDiagnostics("a/go.mod", &d),
 		)
 		env.ApplyQuickFixes("a/go.mod", d.Diagnostics)
@@ -376,7 +376,7 @@
 		env.OpenFile("a/go.mod")
 		var d protocol.PublishDiagnosticsParams
 		env.AfterChange(
-			env.DiagnosticAtRegexp("a/go.mod", `require example.com`),
+			Diagnostics(env.AtRegexp("a/go.mod", `require example.com`)),
 			ReadDiagnostics("a/go.mod", &d),
 		)
 		env.ApplyQuickFixes("a/go.mod", d.Diagnostics)
@@ -436,7 +436,7 @@
 		env.OpenFile("a/main.go")
 		var d protocol.PublishDiagnosticsParams
 		env.AfterChange(
-			env.DiagnosticAtRegexp("a/main.go", `"github.com/esimov/caire"`),
+			Diagnostics(env.AtRegexp("a/main.go", `"github.com/esimov/caire"`)),
 			ReadDiagnostics("a/main.go", &d),
 		)
 		env.ApplyQuickFixes("a/main.go", d.Diagnostics)
@@ -481,7 +481,7 @@
 	}.Run(t, mod, func(t *testing.T, env *Env) {
 		env.OnceMet(
 			InitialWorkspaceLoad,
-			env.DiagnosticAtRegexp("a/go.mod", "require"),
+			Diagnostics(env.AtRegexp("a/go.mod", "require")),
 		)
 		env.RunGoCommandInDir("a", "mod", "tidy")
 		env.AfterChange(
@@ -596,7 +596,7 @@
 		runner.Run(t, unknown, func(t *testing.T, env *Env) {
 			env.OpenFile("a/go.mod")
 			env.Await(
-				env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.2"),
+				Diagnostics(env.AtRegexp("a/go.mod", "example.com v1.2.2")),
 			)
 			env.RegexpReplace("a/go.mod", "v1.2.2", "v1.2.3")
 			env.SaveBuffer("a/go.mod") // Save to trigger diagnostics.
@@ -615,7 +615,7 @@
 			env.SaveBuffer("a/go.mod")  // Save to trigger diagnostics.
 			env.AfterChange(
 				NoDiagnostics(ForFile("a/go.mod")),
-				env.DiagnosticAtRegexp("a/main.go", "x = "),
+				Diagnostics(env.AtRegexp("a/main.go", "x = ")),
 			)
 		})
 	})
@@ -645,17 +645,17 @@
 		runner.Run(t, known, func(t *testing.T, env *Env) {
 			env.OpenFile("a/go.mod")
 			env.AfterChange(
-				env.DiagnosticAtRegexp("a/main.go", "x = "),
+				Diagnostics(env.AtRegexp("a/main.go", "x = ")),
 			)
 			env.RegexpReplace("a/go.mod", "v1.2.3", "v1.2.2")
 			env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk
 			env.AfterChange(
-				env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.2"),
+				Diagnostics(env.AtRegexp("a/go.mod", "example.com v1.2.2")),
 			)
 			env.RegexpReplace("a/go.mod", "v1.2.2", "v1.2.3")
 			env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk
 			env.AfterChange(
-				env.DiagnosticAtRegexp("a/main.go", "x = "),
+				Diagnostics(env.AtRegexp("a/main.go", "x = ")),
 			)
 		})
 	})
@@ -706,7 +706,7 @@
 	}.Run(t, module, func(t *testing.T, env *Env) {
 		env.OpenFile("a/go.mod")
 		env.Await(
-			env.DiagnosticAtRegexp("a/go.mod", "require example.com v1.2.3"),
+			Diagnostics(env.AtRegexp("a/go.mod", "require example.com v1.2.3")),
 		)
 	})
 }
@@ -735,7 +735,7 @@
 		env.OpenFile("main.go")
 		original := env.ReadWorkspaceFile("go.mod")
 		env.Await(
-			env.DiagnosticAtRegexp("main.go", `"example.com/blah"`),
+			Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)),
 		)
 		got := env.ReadWorkspaceFile("go.mod")
 		if got != original {
@@ -820,7 +820,7 @@
 		Settings{"buildFlags": []string{"-tags", "bob"}},
 	).Run(t, mod, func(t *testing.T, env *Env) {
 		env.Await(
-			env.DiagnosticAtRegexp("main.go", `"example.com/blah"`),
+			Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)),
 		)
 	})
 }
@@ -840,7 +840,7 @@
 		env.OpenFile("go.mod")
 		env.RegexpReplace("go.mod", "module", "modul")
 		env.Await(
-			env.DiagnosticAtRegexp("go.mod", "modul"),
+			Diagnostics(env.AtRegexp("go.mod", "modul")),
 		)
 	})
 }
@@ -981,7 +981,7 @@
 			env.OpenFile("go.mod")
 			d := &protocol.PublishDiagnosticsParams{}
 			env.AfterChange(
-				env.DiagnosticAtRegexp("go.mod", "require hasdep.com v1.2.3"),
+				Diagnostics(env.AtRegexp("go.mod", "require hasdep.com v1.2.3")),
 				ReadDiagnostics("go.mod", d),
 			)
 			const want = `module mod.com
diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go
index f3453d7..c81bf1a 100644
--- a/gopls/internal/regtest/template/template_test.go
+++ b/gopls/internal/regtest/template/template_test.go
@@ -73,7 +73,7 @@
 		var diags protocol.PublishDiagnosticsParams
 		env.OnceMet(
 			InitialWorkspaceLoad,
-			env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}"),
+			Diagnostics(env.AtRegexp("hello.tmpl", "()Hello {{}}")),
 			ReadDiagnostics("hello.tmpl", &diags),
 		)
 		d := diags.Diagnostics // issue 50786: check for Source
@@ -120,7 +120,7 @@
 	).Run(t, files, func(t *testing.T, env *Env) {
 		env.OnceMet(
 			InitialWorkspaceLoad,
-			env.DiagnosticAtRegexp("a/a.tmpl", "()A"),
+			Diagnostics(env.AtRegexp("a/a.tmpl", "()A")),
 			NoDiagnostics(ForFile("b/b.tmpl")),
 		)
 	})
@@ -140,7 +140,7 @@
 			NoDiagnostics(ForFile("hello.tmpl")), // Don't get spurious errors for empty templates.
 		)
 		env.SetBufferContent("hello.tmpl", "{{range .Planets}}\nHello {{}}\n{{end}}")
-		env.Await(env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}"))
+		env.Await(Diagnostics(env.AtRegexp("hello.tmpl", "()Hello {{}}")))
 		env.RegexpReplace("hello.tmpl", "{{}}", "{{.}}")
 		env.Await(NoDiagnostics(ForFile("hello.tmpl")))
 	})
@@ -161,7 +161,7 @@
 	Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("hello.tmpl")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}"),
+			Diagnostics(env.AtRegexp("hello.tmpl", "()Hello {{}}")),
 		)
 		// Since we don't have templateExtensions configured, closing hello.tmpl
 		// should make its diagnostics disappear.
diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go
index b445f88..9086483 100644
--- a/gopls/internal/regtest/watch/watch_test.go
+++ b/gopls/internal/regtest/watch/watch_test.go
@@ -39,7 +39,7 @@
 		Run(t, pkg, func(t *testing.T, env *Env) {
 			env.OnceMet(
 				InitialWorkspaceLoad,
-				env.DiagnosticAtRegexp("a/a.go", "x"),
+				Diagnostics(env.AtRegexp("a/a.go", "x")),
 			)
 			env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`)
 			env.AfterChange(
@@ -59,7 +59,7 @@
 			env.AfterChange()
 			env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`)
 			env.AfterChange(
-				env.DiagnosticAtRegexp("a/a.go", "x"),
+				Diagnostics(env.AtRegexp("a/a.go", "x")),
 			)
 		})
 	})
@@ -92,7 +92,7 @@
 		env.Await(env.DoneWithOpen())
 		env.WriteWorkspaceFile("b/b.go", `package b; func B() {};`)
 		env.Await(
-			env.DiagnosticAtRegexp("a/a.go", "b.B"),
+			Diagnostics(env.AtRegexp("a/a.go", "b.B")),
 		)
 	})
 }
@@ -124,7 +124,7 @@
 	Run(t, pkg, func(t *testing.T, env *Env) {
 		env.OnceMet(
 			InitialWorkspaceLoad,
-			env.DiagnosticAtRegexp("a/a.go", "x"),
+			Diagnostics(env.AtRegexp("a/a.go", "x")),
 		)
 		env.WriteWorkspaceFiles(map[string]string{
 			"b/b.go": `package b; func B() {};`,
@@ -170,7 +170,7 @@
 		env.Await(env.DoneWithOpen())
 		env.RemoveWorkspaceFile("b/b.go")
 		env.Await(
-			env.DiagnosticAtRegexp("a/a.go", "\"mod.com/b\""),
+			Diagnostics(env.AtRegexp("a/a.go", "\"mod.com/b\"")),
 		)
 	})
 }
@@ -200,7 +200,7 @@
 	Run(t, missing, func(t *testing.T, env *Env) {
 		env.OnceMet(
 			InitialWorkspaceLoad,
-			env.DiagnosticAtRegexp("a/a.go", "\"mod.com/c\""),
+			Diagnostics(env.AtRegexp("a/a.go", "\"mod.com/c\"")),
 		)
 		env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`)
 		env.AfterChange(
@@ -248,7 +248,7 @@
 	Run(t, pkg, func(t *testing.T, env *Env) {
 		env.OnceMet(
 			InitialWorkspaceLoad,
-			env.DiagnosticAtRegexp("a/a.go", "hello"),
+			Diagnostics(env.AtRegexp("a/a.go", "hello")),
 		)
 		env.WriteWorkspaceFile("a/a2.go", `package a; func hello() {};`)
 		env.AfterChange(
@@ -392,7 +392,7 @@
 			env.RemoveWorkspaceFile("a/a_unneeded.go")
 			env.RegexpReplace("a/a.go", "var _ int", "fmt.Println(\"\")")
 			env.AfterChange(
-				env.DiagnosticAtRegexp("a/a.go", "fmt"),
+				Diagnostics(env.AtRegexp("a/a.go", "fmt")),
 			)
 			env.SaveBuffer("a/a.go")
 			env.AfterChange(
@@ -420,7 +420,7 @@
 			env.CloseBuffer("a/a_unneeded.go")
 			env.RegexpReplace("a/a.go", "var _ int", "fmt.Println(\"\")")
 			env.Await(
-				env.DiagnosticAtRegexp("a/a.go", "fmt"),
+				Diagnostics(env.AtRegexp("a/a.go", "fmt")),
 			)
 			env.SaveBuffer("a/a.go")
 			env.AfterChange(
@@ -621,7 +621,7 @@
 		env.OpenFile("foo/main.go")
 		env.RemoveWorkspaceFile("foo/go.mod")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("foo/main.go", `"mod.com/blah"`),
+			Diagnostics(env.AtRegexp("foo/main.go", `"mod.com/blah"`)),
 		)
 		env.RegexpReplace("foo/main.go", `"mod.com/blah"`, `"foo/blah"`)
 		env.AfterChange(
diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go
index 0c718b3..5c5a68a 100644
--- a/gopls/internal/regtest/workspace/broken_test.go
+++ b/gopls/internal/regtest/workspace/broken_test.go
@@ -161,7 +161,7 @@
 	Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("p/internal/bar/bar.go")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("p/internal/bar/bar.go", "\"mod.test/p/internal/foo\""),
+			Diagnostics(env.AtRegexp("p/internal/bar/bar.go", "\"mod.test/p/internal/foo\"")),
 		)
 		env.OpenFile("go.mod")
 		env.RegexpReplace("go.mod", "mod.testx", "mod.test")
@@ -207,8 +207,8 @@
 				env.OpenFile("a/empty.go")
 				env.OpenFile("b/go.mod")
 				env.AfterChange(
-					env.DiagnosticAtRegexp("a/a.go", "package a"),
-					env.DiagnosticAtRegexp("b/go.mod", "module b.com"),
+					Diagnostics(env.AtRegexp("a/a.go", "package a")),
+					Diagnostics(env.AtRegexp("b/go.mod", "module b.com")),
 					OutstandingWork(lsp.WorkspaceLoadFailure, msg),
 				)
 
@@ -238,9 +238,9 @@
 				env.OpenFile("b/b.go")
 				env.AfterChange(
 					// TODO(rfindley): fix these missing diagnostics.
-					// env.DiagnosticAtRegexp("a/a.go", "package a"),
-					// env.DiagnosticAtRegexp("b/go.mod", "module b.com"),
-					env.DiagnosticAtRegexp("b/b.go", "package b"),
+					// Diagnostics(env.AtRegexp("a/a.go", "package a")),
+					// Diagnostics(env.AtRegexp("b/go.mod", "module b.com")),
+					Diagnostics(env.AtRegexp("b/b.go", "package b")),
 					OutstandingWork(lsp.WorkspaceLoadFailure, msg),
 				)
 			})
diff --git a/gopls/internal/regtest/workspace/directoryfilters_test.go b/gopls/internal/regtest/workspace/directoryfilters_test.go
index f7c7153..5db8d89 100644
--- a/gopls/internal/regtest/workspace/directoryfilters_test.go
+++ b/gopls/internal/regtest/workspace/directoryfilters_test.go
@@ -135,7 +135,7 @@
 		ProxyFiles(proxy),
 		Settings{"directoryFilters": []string{"-exclude"}},
 	).Run(t, files, func(t *testing.T, env *Env) {
-		env.Await(env.DiagnosticAtRegexp("include/include.go", `exclude.(X)`))
+		env.Await(Diagnostics(env.AtRegexp("include/include.go", `exclude.(X)`)))
 	})
 }
 
diff --git a/gopls/internal/regtest/workspace/metadata_test.go b/gopls/internal/regtest/workspace/metadata_test.go
index 31663fb..11dfe67 100644
--- a/gopls/internal/regtest/workspace/metadata_test.go
+++ b/gopls/internal/regtest/workspace/metadata_test.go
@@ -73,8 +73,8 @@
 		env.OpenFile("bar.go")
 		env.OnceMet(
 			env.DoneWithOpen(),
-			env.DiagnosticAtRegexp("foo.go", "func (main)"),
-			env.DiagnosticAtRegexp("bar.go", "func (main)"),
+			Diagnostics(env.AtRegexp("foo.go", "func (main)")),
+			Diagnostics(env.AtRegexp("bar.go", "func (main)")),
 		)
 
 		// Ignore bar.go. This should resolve diagnostics.
diff --git a/gopls/internal/regtest/workspace/standalone_test.go b/gopls/internal/regtest/workspace/standalone_test.go
index 7056f54..d7f0346 100644
--- a/gopls/internal/regtest/workspace/standalone_test.go
+++ b/gopls/internal/regtest/workspace/standalone_test.go
@@ -131,7 +131,7 @@
 		// Renaming "lib.C" to "lib.D" should cause a diagnostic in the standalone
 		// file.
 		env.RegexpReplace("lib/lib.go", "C", "D")
-		env.AfterChange(env.DiagnosticAtRegexp("lib/ignore.go", "lib.(C)"))
+		env.AfterChange(Diagnostics(env.AtRegexp("lib/ignore.go", "lib.(C)")))
 
 		// Undoing the replacement should fix diagnostics
 		env.RegexpReplace("lib/lib.go", "D", "C")
@@ -186,7 +186,7 @@
 		env.OpenFile("standalone.go")
 
 		env.AfterChange(
-			env.DiagnosticAtRegexp("ignore.go", "package (main)"),
+			Diagnostics(env.AtRegexp("ignore.go", "package (main)")),
 			NoDiagnostics(ForFile("standalone.go")),
 		)
 
@@ -202,7 +202,7 @@
 
 		env.AfterChange(
 			NoDiagnostics(ForFile("ignore.go")),
-			env.DiagnosticAtRegexp("standalone.go", "package (main)"),
+			Diagnostics(env.AtRegexp("standalone.go", "package (main)")),
 		)
 	})
 }
diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go
index 3932648..0573455 100644
--- a/gopls/internal/regtest/workspace/workspace_test.go
+++ b/gopls/internal/regtest/workspace/workspace_test.go
@@ -152,7 +152,7 @@
 	).Run(t, workspaceModule, func(t *testing.T, env *Env) {
 		env.OpenFile("pkg/main.go")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("pkg/main2.go", "fmt.Print"),
+			Diagnostics(env.AtRegexp("pkg/main2.go", "fmt.Print")),
 		)
 		env.CloseBuffer("pkg/main.go")
 		env.AfterChange(
@@ -267,8 +267,8 @@
 		env.RunGoCommand("work", "init")
 		env.RunGoCommand("work", "use", "-r", ".")
 		env.AfterChange(
-			env.DiagnosticAtRegexp("moda/a/a.go", "x"),
-			env.DiagnosticAtRegexp("modb/b/b.go", "x"),
+			Diagnostics(env.AtRegexp("moda/a/a.go", "x")),
+			Diagnostics(env.AtRegexp("modb/b/b.go", "x")),
 			NoDiagnostics(env.AtRegexp("moda/a/a.go", `"b.com/b"`)),
 		)
 	})
@@ -324,7 +324,7 @@
 		ProxyFiles(proxy),
 	).Run(t, multiModule, func(t *testing.T, env *Env) {
 		env.Await(
-			env.DiagnosticAtRegexp("main.go", "x"),
+			Diagnostics(env.AtRegexp("main.go", "x")),
 		)
 	})
 }
@@ -452,7 +452,7 @@
 }
 `,
 		})
-		env.AfterChange(env.DiagnosticAtRegexp("modb/b/b.go", "x"))
+		env.AfterChange(Diagnostics(env.AtRegexp("modb/b/b.go", "x")))
 		got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
 		if want := "modb/b/b.go"; !strings.HasSuffix(got, want) {
 			t.Errorf("expected %s, got %v", want, original)
@@ -508,7 +508,7 @@
 		env.RegexpReplace("modb/go.mod", "modul", "module")
 		env.SaveBufferWithoutActions("modb/go.mod")
 		env.Await(
-			env.DiagnosticAtRegexp("modb/b/b.go", "x"),
+			Diagnostics(env.AtRegexp("modb/b/b.go", "x")),
 		)
 	})
 }
@@ -607,7 +607,7 @@
 		// As of golang/go#54069, writing go.work to the workspace triggers a
 		// workspace reload.
 		env.AfterChange(
-			env.DiagnosticAtRegexp("modb/b/b.go", "x"),
+			Diagnostics(env.AtRegexp("modb/b/b.go", "x")),
 		)
 
 		// Jumping to definition should now go to b.com in the workspace.
@@ -905,10 +905,10 @@
 			InitialWorkspaceLoad,
 			// TODO(rfindley): assert on the full set of diagnostics here. We
 			// should ensure that we don't have a diagnostic at b.Hi in a.go.
-			env.DiagnosticAtRegexp("moda/a/a.go", "x"),
-			env.DiagnosticAtRegexp("modb/b/b.go", "x"),
-			env.DiagnosticAtRegexp("modb/v2/b/b.go", "x"),
-			env.DiagnosticAtRegexp("modc/main.go", "x"),
+			Diagnostics(env.AtRegexp("moda/a/a.go", "x")),
+			Diagnostics(env.AtRegexp("modb/b/b.go", "x")),
+			Diagnostics(env.AtRegexp("modb/v2/b/b.go", "x")),
+			Diagnostics(env.AtRegexp("modc/main.go", "x")),
 		)
 	})
 }
@@ -1114,8 +1114,8 @@
 		// as invalid. So we need to wait for the metadata of main_test.go to be
 		// updated before moving other_test.go back to the main_test package.
 		env.Await(
-			env.DiagnosticAtRegexp("other_test.go", "Server"),
-			env.DiagnosticAtRegexp("main_test.go", "otherConst"),
+			Diagnostics(env.AtRegexp("other_test.go", "Server")),
+			Diagnostics(env.AtRegexp("main_test.go", "otherConst")),
 		)
 		env.RegexpReplace("other_test.go", "main", "main_test")
 		env.AfterChange(