internal/lsp: rewrite the workspace symbol marker tests

The workspace symbol tests are not really resilient to changes and
did not generate/use golden files consistently. This made it really
tricky to switch the workspace symbols defaults.

To improve the workflow, consolidate the different kinds of tests into
one function, generate and use golden files, and require that all of the
workspace symbol queries appear in one file only. Also converted the
brittle workspace symbol regtest to a marker test.

Update golang/go#41760

Change-Id: I41ccd3ae58ae08fea717c7d8e9a2a10330e8c14f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/271626
Trust: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
diff --git a/gopls/internal/regtest/symbol_helper_test.go b/gopls/internal/regtest/symbol_helper_test.go
deleted file mode 100644
index c4ece70..0000000
--- a/gopls/internal/regtest/symbol_helper_test.go
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright 2020 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package regtest
-
-import (
-	"encoding/json"
-	"fmt"
-
-	"golang.org/x/tools/internal/lsp/fake"
-	"golang.org/x/tools/internal/lsp/protocol"
-)
-
-// expSymbolInformation and the types it references are pointer-based versions
-// of fake.SymbolInformation, used to make it easier to partially assert
-// against values of type fake.SymbolInformation
-
-// expSymbolInformation is a pointer-based version of fake.SymbolInformation
-type expSymbolInformation struct {
-	Name     *string
-	Kind     *protocol.SymbolKind
-	Location *expLocation
-}
-
-func (e *expSymbolInformation) matchAgainst(sis []fake.SymbolInformation) bool {
-	for _, si := range sis {
-		if e.match(si) {
-			return true
-		}
-	}
-	return false
-}
-
-func (e *expSymbolInformation) match(si fake.SymbolInformation) bool {
-	if e.Name != nil && *e.Name != si.Name {
-		return false
-	}
-	if e.Kind != nil && *e.Kind != si.Kind {
-		return false
-	}
-	if e.Location != nil && !e.Location.match(si.Location) {
-		return false
-	}
-	return true
-}
-
-func (e *expSymbolInformation) String() string {
-	byts, err := json.MarshalIndent(e, "", "  ")
-	if err != nil {
-		panic(fmt.Errorf("failed to json.Marshal *expSymbolInformation: %v", err))
-	}
-	return string(byts)
-}
-
-// expLocation is a pointer-based version of fake.Location
-type expLocation struct {
-	Path  *string
-	Range *expRange
-}
-
-func (e *expLocation) match(l fake.Location) bool {
-	if e.Path != nil && *e.Path != l.Path {
-		return false
-	}
-	if e.Range != nil && !e.Range.match(l.Range) {
-		return false
-	}
-	return true
-}
-
-// expRange is a pointer-based version of fake.Range
-type expRange struct {
-	Start *expPos
-	End   *expPos
-}
-
-func (e *expRange) match(l fake.Range) bool {
-	if e.Start != nil && !e.Start.match(l.Start) {
-		return false
-	}
-	if e.End != nil && !e.End.match(l.End) {
-		return false
-	}
-	return true
-}
-
-// expPos is a pointer-based version of fake.Pos
-type expPos struct {
-	Line   *int
-	Column *int
-}
-
-func (e *expPos) match(l fake.Pos) bool {
-	if e.Line != nil && *e.Line != l.Line {
-		return false
-	}
-	if e.Column != nil && *e.Column != l.Column {
-		return false
-	}
-	return true
-}
-
-func pString(s string) *string {
-	return &s
-}
-
-func pInt(i int) *int {
-	return &i
-}
-
-func pKind(k protocol.SymbolKind) *protocol.SymbolKind {
-	return &k
-}
diff --git a/gopls/internal/regtest/symbol_test.go b/gopls/internal/regtest/symbol_test.go
deleted file mode 100644
index ecb6652..0000000
--- a/gopls/internal/regtest/symbol_test.go
+++ /dev/null
@@ -1,282 +0,0 @@
-// Copyright 2020 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package regtest
-
-import (
-	"testing"
-
-	"golang.org/x/tools/internal/lsp/protocol"
-)
-
-const symbolSetup = `
--- go.mod --
-module mod.com
-
-go 1.12
--- main.go --
-package main
-
-import (
-	"encoding/json"
-	"fmt"
-)
-
-func main() { // function
-	fmt.Println("Hello")
-}
-
-var myvar int // variable
-
-type myType string // basic type
-
-type myDecoder json.Decoder // to use the encoding/json import
-
-func (m *myType) Blahblah() {} // method
-
-type myStruct struct { // struct type
-	myStructField int // struct field
-}
-
-type myInterface interface { // interface
-	DoSomeCoolStuff() string // interface method
-}
-
-type embed struct {
-	myStruct
-
-	nestedStruct struct {
-		nestedField int
-
-		nestedStruct2 struct {
-			int
-		}
-	}
-
-	nestedInterface interface {
-		myInterface
-		nestedMethod()
-	}
-}
--- p/p.go --
-package p
-
-const Message = "Hello World." // constant
-`
-
-var caseSensitiveSymbolChecks = map[string]*expSymbolInformation{
-	"main": {
-		Name: pString("main.main"),
-		Kind: pKind(protocol.Function),
-		Location: &expLocation{
-			Path: pString("main.go"),
-			Range: &expRange{
-				Start: &expPos{
-					Line:   pInt(7),
-					Column: pInt(5),
-				},
-			},
-		},
-	},
-	"Message": {
-		Name: pString("p.Message"),
-		Kind: pKind(protocol.Constant),
-		Location: &expLocation{
-			Path: pString("p/p.go"),
-			Range: &expRange{
-				Start: &expPos{
-					Line:   pInt(2),
-					Column: pInt(6),
-				},
-			},
-		},
-	},
-	"myvar": {
-		Name: pString("main.myvar"),
-		Kind: pKind(protocol.Variable),
-		Location: &expLocation{
-			Path: pString("main.go"),
-			Range: &expRange{
-				Start: &expPos{
-					Line:   pInt(11),
-					Column: pInt(4),
-				},
-			},
-		},
-	},
-	"myType": {
-		Name: pString("main.myType"),
-		Kind: pKind(protocol.String),
-		Location: &expLocation{
-			Path: pString("main.go"),
-			Range: &expRange{
-				Start: &expPos{
-					Line:   pInt(13),
-					Column: pInt(5),
-				},
-			},
-		},
-	},
-	"Blahblah": {
-		Name: pString("main.myType.Blahblah"),
-		Kind: pKind(protocol.Method),
-		Location: &expLocation{
-			Path: pString("main.go"),
-			Range: &expRange{
-				Start: &expPos{
-					Line:   pInt(17),
-					Column: pInt(17),
-				},
-			},
-		},
-	},
-	"NewEncoder": {
-		Name: pString("json.NewEncoder"),
-		Kind: pKind(protocol.Function),
-	},
-	"myStruct": {
-		Name: pString("main.myStruct"),
-		Kind: pKind(protocol.Struct),
-		Location: &expLocation{
-			Path: pString("main.go"),
-			Range: &expRange{
-				Start: &expPos{
-					Line:   pInt(19),
-					Column: pInt(5),
-				},
-			},
-		},
-	},
-	// TODO: not sure we should be returning struct fields
-	"myStructField": {
-		Name: pString("main.myStruct.myStructField"),
-		Kind: pKind(protocol.Field),
-		Location: &expLocation{
-			Path: pString("main.go"),
-			Range: &expRange{
-				Start: &expPos{
-					Line:   pInt(20),
-					Column: pInt(1),
-				},
-			},
-		},
-	},
-	"myInterface": {
-		Name: pString("main.myInterface"),
-		Kind: pKind(protocol.Interface),
-		Location: &expLocation{
-			Path: pString("main.go"),
-			Range: &expRange{
-				Start: &expPos{
-					Line:   pInt(23),
-					Column: pInt(5),
-				},
-			},
-		},
-	},
-	// TODO: not sure we should be returning interface methods
-	"DoSomeCoolStuff": {
-		Name: pString("main.myInterface.DoSomeCoolStuff"),
-		Kind: pKind(protocol.Method),
-		Location: &expLocation{
-			Path: pString("main.go"),
-			Range: &expRange{
-				Start: &expPos{
-					Line:   pInt(24),
-					Column: pInt(1),
-				},
-			},
-		},
-	},
-
-	"embed.myStruct": {
-		Name: pString("main.embed.myStruct"),
-		Kind: pKind(protocol.Field),
-		Location: &expLocation{
-			Path: pString("main.go"),
-			Range: &expRange{
-				Start: &expPos{
-					Line:   pInt(28),
-					Column: pInt(1),
-				},
-			},
-		},
-	},
-
-	"nestedStruct2.int": {
-		Name: pString("main.embed.nestedStruct.nestedStruct2.int"),
-		Kind: pKind(protocol.Field),
-		Location: &expLocation{
-			Path: pString("main.go"),
-			Range: &expRange{
-				Start: &expPos{
-					Line:   pInt(34),
-					Column: pInt(3),
-				},
-			},
-		},
-	},
-
-	"nestedInterface.myInterface": {
-		Name: pString("main.embed.nestedInterface.myInterface"),
-		Kind: pKind(protocol.Interface),
-		Location: &expLocation{
-			Path: pString("main.go"),
-			Range: &expRange{
-				Start: &expPos{
-					Line:   pInt(39),
-					Column: pInt(2),
-				},
-			},
-		},
-	},
-
-	"nestedInterface.nestedMethod": {
-		Name: pString("main.embed.nestedInterface.nestedMethod"),
-		Kind: pKind(protocol.Method),
-		Location: &expLocation{
-			Path: pString("main.go"),
-			Range: &expRange{
-				Start: &expPos{
-					Line:   pInt(40),
-					Column: pInt(2),
-				},
-			},
-		},
-	},
-}
-
-var caseInsensitiveSymbolChecks = map[string]*expSymbolInformation{
-	"Main": caseSensitiveSymbolChecks["main"],
-}
-
-var fuzzySymbolChecks = map[string]*expSymbolInformation{
-	"Mn": caseSensitiveSymbolChecks["main"],
-}
-
-// TestSymbolPos tests that, at a basic level, we get the correct position
-// information for symbols matches that are returned.
-func TestSymbolPos(t *testing.T) {
-	checkChecks(t, "caseSensitive", caseSensitiveSymbolChecks)
-	checkChecks(t, "caseInsensitive", caseInsensitiveSymbolChecks)
-	checkChecks(t, "fuzzy", fuzzySymbolChecks)
-}
-
-func checkChecks(t *testing.T, matcher string, checks map[string]*expSymbolInformation) {
-	t.Helper()
-	withOptions(
-		EditorConfig{SymbolMatcher: &matcher},
-	).run(t, symbolSetup, func(t *testing.T, env *Env) {
-		t.Run(matcher, func(t *testing.T) {
-			for query, exp := range checks {
-				t.Run(query, func(t *testing.T) {
-					res := env.Symbol(query)
-					if !exp.matchAgainst(res) {
-						t.Fatalf("failed to find a match against query %q for %v,\ngot: %v", query, exp, res)
-					}
-				})
-			}
-		})
-	})
-}
diff --git a/internal/lsp/cmd/test/workspace_symbol.go b/internal/lsp/cmd/test/workspace_symbol.go
index 96d9504..f52ee11 100644
--- a/internal/lsp/cmd/test/workspace_symbol.go
+++ b/internal/lsp/cmd/test/workspace_symbol.go
@@ -5,44 +5,45 @@
 package cmdtest
 
 import (
-	"path"
+	"fmt"
+	"path/filepath"
 	"sort"
 	"strings"
 	"testing"
 
-	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/lsp/tests"
+	"golang.org/x/tools/internal/span"
 )
 
-func (r *runner) WorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
-	r.runWorkspaceSymbols(t, "caseInsensitive", query, dirs)
+func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) {
+	var matcher string
+	switch typ {
+	case tests.WorkspaceSymbolsFuzzy:
+		matcher = "fuzzy"
+	case tests.WorkspaceSymbolsCaseSensitive:
+		matcher = "caseSensitive"
+	case tests.WorkspaceSymbolsDefault:
+		matcher = "caseInsensitive"
+	}
+	r.runWorkspaceSymbols(t, uri, matcher, query)
 }
 
-func (r *runner) FuzzyWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
-	r.runWorkspaceSymbols(t, "fuzzy", query, dirs)
-}
-
-func (r *runner) CaseSensitiveWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
-	r.runWorkspaceSymbols(t, "caseSensitive", query, dirs)
-}
-
-func (r *runner) runWorkspaceSymbols(t *testing.T, matcher, query string, dirs map[string]struct{}) {
+func (r *runner) runWorkspaceSymbols(t *testing.T, uri span.URI, matcher, query string) {
 	t.Helper()
 
 	out, _ := r.runGoplsCmd(t, "workspace_symbol", "-matcher", matcher, query)
 	var filtered []string
+	dir := filepath.Dir(uri.Filename())
 	for _, line := range strings.Split(out, "\n") {
-		for dir := range dirs {
-			if strings.HasPrefix(line, dir) {
-				filtered = append(filtered, line)
-				break
-			}
+		if source.InDir(dir, line) {
+			filtered = append(filtered, filepath.ToSlash(line))
 		}
 	}
 	sort.Strings(filtered)
-	got := r.Normalize(strings.Join(filtered, "\n"))
+	got := r.Normalize(strings.Join(filtered, "\n") + "\n")
 
-	expect := string(r.data.Golden("workspace_symbol", workspaceSymbolsGolden(matcher, query), func() ([]byte, error) {
+	expect := string(r.data.Golden(fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) {
 		return []byte(got), nil
 	}))
 
@@ -50,29 +51,3 @@
 		t.Errorf("workspace_symbol failed for %s:\n%s", query, tests.Diff(expect, got))
 	}
 }
-
-var workspaceSymbolsDir = map[string]string{
-	// TODO: make caseInsensitive test cases consistent with
-	// other matcher.
-	"caseInsensitive": "",
-	"fuzzy":           "fuzzy",
-	"caseSensitive":   "casesensitive",
-}
-
-func workspaceSymbolsGolden(matcher, query string) string {
-	dir := []string{"workspacesymbol", workspaceSymbolsDir[matcher]}
-	if query == "" {
-		return path.Join(append(dir, "EmptyQuery")...)
-	}
-
-	var name []rune
-	for _, r := range query {
-		if 'A' <= r && r <= 'Z' {
-			// Escape uppercase to '!' + lowercase for case insensitive file systems.
-			name = append(name, '!', r+'a'-'A')
-		} else {
-			name = append(name, r)
-		}
-	}
-	return path.Join(append(dir, string(name))...)
-}
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 2d3ecee..f9a3cb2 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -12,6 +12,7 @@
 	"os/exec"
 	"path/filepath"
 	"sort"
+	"strings"
 	"testing"
 
 	"golang.org/x/tools/internal/lsp/cache"
@@ -1014,21 +1015,15 @@
 	}
 }
 
-func (r *runner) WorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
-	r.callWorkspaceSymbols(t, query, source.SymbolCaseInsensitive, dirs, expectedSymbols)
+func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) {
+	r.callWorkspaceSymbols(t, uri, query, typ)
 }
 
-func (r *runner) FuzzyWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
-	r.callWorkspaceSymbols(t, query, source.SymbolFuzzy, dirs, expectedSymbols)
-}
-
-func (r *runner) CaseSensitiveWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
-	r.callWorkspaceSymbols(t, query, source.SymbolCaseSensitive, dirs, expectedSymbols)
-}
-
-func (r *runner) callWorkspaceSymbols(t *testing.T, query string, matcher source.SymbolMatcher, dirs map[string]struct{}, expectedSymbols []protocol.SymbolInformation) {
+func (r *runner) callWorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) {
 	t.Helper()
 
+	matcher := tests.WorkspaceSymbolsTestTypeToMatcher(typ)
+
 	original := r.server.session.Options()
 	modified := original
 	modified.SymbolMatcher = matcher
@@ -1038,12 +1033,19 @@
 	params := &protocol.WorkspaceSymbolParams{
 		Query: query,
 	}
-	got, err := r.server.Symbol(r.ctx, params)
+	gotSymbols, err := r.server.Symbol(r.ctx, params)
 	if err != nil {
 		t.Fatal(err)
 	}
-	got = tests.FilterWorkspaceSymbols(got, dirs)
-	if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
+	got, err := tests.WorkspaceSymbolsString(r.ctx, r.data, uri, gotSymbols)
+	if err != nil {
+		t.Fatal(err)
+	}
+	got = filepath.ToSlash(tests.Normalize(got, r.normalizers))
+	want := string(r.data.Golden(fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) {
+		return []byte(got), nil
+	}))
+	if diff := tests.Diff(want, got); diff != "" {
 		t.Error(diff)
 	}
 }
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index 2726b5e..2d79960 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -63,6 +63,7 @@
 	// TODO(golang/go#38212): Delete this once they are enabled by default.
 	tests.EnableAllAnalyzers(view, options)
 	view.SetOptions(ctx, options)
+
 	var modifications []source.FileModification
 	for filename, content := range datum.Config.Overlay {
 		kind := source.DetectLanguage("", filename)
@@ -864,26 +865,27 @@
 	}
 }
 
-func (r *runner) WorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
-	r.callWorkspaceSymbols(t, query, source.SymbolCaseInsensitive, dirs, expectedSymbols)
+func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) {
+	r.callWorkspaceSymbols(t, uri, query, typ)
 }
 
-func (r *runner) FuzzyWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
-	r.callWorkspaceSymbols(t, query, source.SymbolFuzzy, dirs, expectedSymbols)
-}
-
-func (r *runner) CaseSensitiveWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
-	r.callWorkspaceSymbols(t, query, source.SymbolCaseSensitive, dirs, expectedSymbols)
-}
-
-func (r *runner) callWorkspaceSymbols(t *testing.T, query string, matcher source.SymbolMatcher, dirs map[string]struct{}, expectedSymbols []protocol.SymbolInformation) {
+func (r *runner) callWorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) {
 	t.Helper()
-	got, err := source.WorkspaceSymbols(r.ctx, matcher, source.PackageQualifiedSymbols, []source.View{r.view}, query)
+
+	matcher := tests.WorkspaceSymbolsTestTypeToMatcher(typ)
+	gotSymbols, err := source.WorkspaceSymbols(r.ctx, matcher, r.view.Options().SymbolStyle, []source.View{r.view}, query)
 	if err != nil {
 		t.Fatal(err)
 	}
-	got = tests.FilterWorkspaceSymbols(got, dirs)
-	if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
+	got, err := tests.WorkspaceSymbolsString(r.ctx, r.data, uri, gotSymbols)
+	if err != nil {
+		t.Fatal(err)
+	}
+	got = filepath.ToSlash(tests.Normalize(got, r.normalizers))
+	want := string(r.data.Golden(fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) {
+		return []byte(got), nil
+	}))
+	if diff := tests.Diff(want, got); diff != "" {
 		t.Error(diff)
 	}
 }
diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden
index a1daa7f..0f327f6 100644
--- a/internal/lsp/testdata/summary.txt.golden
+++ b/internal/lsp/testdata/summary.txt.golden
@@ -22,9 +22,7 @@
 RenamesCount = 30
 PrepareRenamesCount = 7
 SymbolsCount = 5
-WorkspaceSymbolsCount = 2
-FuzzyWorkspaceSymbolsCount = 3
-CaseSensitiveWorkspaceSymbolsCount = 2
+WorkspaceSymbolsCount = 20
 SignaturesCount = 32
 LinksCount = 7
 ImplementationsCount = 14
diff --git a/internal/lsp/testdata/workspacesymbol/EmptyQuery.golden b/internal/lsp/testdata/workspacesymbol/EmptyQuery.golden
deleted file mode 100644
index 6f5d587..0000000
--- a/internal/lsp/testdata/workspacesymbol/EmptyQuery.golden
+++ /dev/null
@@ -1,2 +0,0 @@
--- workspace_symbol --
-
diff --git "a/internal/lsp/testdata/workspacesymbol/casesensitive/\041dunk.golden" "b/internal/lsp/testdata/workspacesymbol/casesensitive/\041dunk.golden"
deleted file mode 100644
index 9b7b8b7..0000000
--- "a/internal/lsp/testdata/workspacesymbol/casesensitive/\041dunk.golden"
+++ /dev/null
@@ -1,2 +0,0 @@
--- workspace_symbol --
-symbols/main.go:62:6-10 main.Dunk Function
diff --git a/internal/lsp/testdata/workspacesymbol/casesensitive/casesensitive.go b/internal/lsp/testdata/workspacesymbol/casesensitive/casesensitive.go
deleted file mode 100644
index 10d1ebc..0000000
--- a/internal/lsp/testdata/workspacesymbol/casesensitive/casesensitive.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package casesensitive
-
-/*@
-workspacesymbolcasesensitive("dunk", dunk)
-workspacesymbolcasesensitive("Dunk", Dunk)
-*/
diff --git a/internal/lsp/testdata/workspacesymbol/casesensitive/dunk.golden b/internal/lsp/testdata/workspacesymbol/casesensitive/dunk.golden
deleted file mode 100644
index 018e3a8..0000000
--- a/internal/lsp/testdata/workspacesymbol/casesensitive/dunk.golden
+++ /dev/null
@@ -1,2 +0,0 @@
--- workspace_symbol --
-symbols/main.go:64:6-10 main.dunk Function
diff --git a/internal/lsp/testdata/workspacesymbol/fuzzy/fuzzy.go b/internal/lsp/testdata/workspacesymbol/fuzzy/fuzzy.go
deleted file mode 100644
index 929bb3b..0000000
--- a/internal/lsp/testdata/workspacesymbol/fuzzy/fuzzy.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package fuzzy
-
-/*@
-workspacesymbolfuzzy("rgop",
-	bBar,
-	randomgopherinvariable,
-	RandomGopherVariableA,
-	RandomGopherXTestVariableA,
-	RandomGopherVariableB,
-	RandomGopherStructB,
-	RandomGopherTestVariableA,
-	RandomGopherConstantA,
-)
-workspacesymbolfuzzy("randoma",
-	bBar,
-	RandomGopherVariableB,
-	randomgopherinvariable,
-	RandomGopherXTestVariableA,
-	RandomGopherTestVariableA,
-	RandomGopherConstantA,
-	RandomGopherVariableA,
-)
-workspacesymbolfuzzy("randomb",
-	bBar,
-	randomgopherinvariable,
-	RandomGopherTestVariableA,
-	RandomGopherXTestVariableA,
-	RandomGopherVariableA,
-	RandomGopherStructB,
-	RandomGopherVariableB,
-)
-*/
diff --git a/internal/lsp/testdata/workspacesymbol/fuzzy/randoma.golden b/internal/lsp/testdata/workspacesymbol/fuzzy/randoma.golden
deleted file mode 100644
index 5e3d1eb..0000000
--- a/internal/lsp/testdata/workspacesymbol/fuzzy/randoma.golden
+++ /dev/null
@@ -1,8 +0,0 @@
--- workspace_symbol --
-workspacesymbol/a/a.go:3:5-26 a.RandomGopherVariableA Variable
-workspacesymbol/a/a.go:5:7-28 a.RandomGopherConstantA Constant
-workspacesymbol/a/a.go:8:2-24 a.randomgopherinvariable Constant
-workspacesymbol/a/a_test.go:3:5-30 a.RandomGopherTestVariableA Variable
-workspacesymbol/a/a_x_test.go:3:5-31 a_test.RandomGopherXTestVariableA Variable
-workspacesymbol/b/b.go:3:5-26 b.RandomGopherVariableB Variable
-workspacesymbol/b/b.go:6:2-5 b.RandomGopherStructB.Bar Field
diff --git a/internal/lsp/testdata/workspacesymbol/fuzzy/randomb.golden b/internal/lsp/testdata/workspacesymbol/fuzzy/randomb.golden
deleted file mode 100644
index 46aafd5..0000000
--- a/internal/lsp/testdata/workspacesymbol/fuzzy/randomb.golden
+++ /dev/null
@@ -1,8 +0,0 @@
--- workspace_symbol --
-workspacesymbol/a/a.go:3:5-26 a.RandomGopherVariableA Variable
-workspacesymbol/a/a.go:8:2-24 a.randomgopherinvariable Constant
-workspacesymbol/a/a_test.go:3:5-30 a.RandomGopherTestVariableA Variable
-workspacesymbol/a/a_x_test.go:3:5-31 a_test.RandomGopherXTestVariableA Variable
-workspacesymbol/b/b.go:3:5-26 b.RandomGopherVariableB Variable
-workspacesymbol/b/b.go:5:6-25 b.RandomGopherStructB Struct
-workspacesymbol/b/b.go:6:2-5 b.RandomGopherStructB.Bar Field
diff --git a/internal/lsp/testdata/workspacesymbol/fuzzy/rgop.golden b/internal/lsp/testdata/workspacesymbol/fuzzy/rgop.golden
deleted file mode 100644
index 1d106f0..0000000
--- a/internal/lsp/testdata/workspacesymbol/fuzzy/rgop.golden
+++ /dev/null
@@ -1,9 +0,0 @@
--- workspace_symbol --
-workspacesymbol/a/a.go:3:5-26 a.RandomGopherVariableA Variable
-workspacesymbol/a/a.go:5:7-28 a.RandomGopherConstantA Constant
-workspacesymbol/a/a.go:8:2-24 a.randomgopherinvariable Constant
-workspacesymbol/a/a_test.go:3:5-30 a.RandomGopherTestVariableA Variable
-workspacesymbol/a/a_x_test.go:3:5-31 a_test.RandomGopherXTestVariableA Variable
-workspacesymbol/b/b.go:3:5-26 b.RandomGopherVariableB Variable
-workspacesymbol/b/b.go:5:6-25 b.RandomGopherStructB Struct
-workspacesymbol/b/b.go:6:2-5 b.RandomGopherStructB.Bar Field
diff --git a/internal/lsp/testdata/workspacesymbol/main.go b/internal/lsp/testdata/workspacesymbol/main.go
new file mode 100644
index 0000000..36ec8f1
--- /dev/null
+++ b/internal/lsp/testdata/workspacesymbol/main.go
@@ -0,0 +1,47 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+)
+
+func main() { // function
+	fmt.Println("Hello")
+}
+
+var myvar int // variable
+
+type myType string // basic type
+
+type myDecoder json.Decoder // to use the encoding/json import
+
+func (m *myType) Blahblah() {} // method
+
+type myStruct struct { // struct type
+	myStructField int // struct field
+}
+
+type myInterface interface { // interface
+	DoSomeCoolStuff() string // interface method
+}
+
+type embed struct {
+	myStruct
+
+	nestedStruct struct {
+		nestedField int
+
+		nestedStruct2 struct {
+			int
+		}
+	}
+
+	nestedInterface interface {
+		myInterface
+		nestedMethod()
+	}
+}
+
+func Dunk() int { return 0 }
+
+func dunk() {}
diff --git a/internal/lsp/testdata/workspacesymbol/p/p.go b/internal/lsp/testdata/workspacesymbol/p/p.go
new file mode 100644
index 0000000..409cc35
--- /dev/null
+++ b/internal/lsp/testdata/workspacesymbol/p/p.go
@@ -0,0 +1,3 @@
+package p
+
+const Message = "Hello World." // constant
diff --git a/internal/lsp/testdata/workspacesymbol/query.go b/internal/lsp/testdata/workspacesymbol/query.go
new file mode 100644
index 0000000..883aae2
--- /dev/null
+++ b/internal/lsp/testdata/workspacesymbol/query.go
@@ -0,0 +1,29 @@
+package main
+
+// Contains all of the workspace symbol queries.
+
+// -- Fuzzy matching --
+//@workspacesymbolfuzzy("rgop")
+//@workspacesymbolfuzzy("randoma")
+//@workspacesymbolfuzzy("randomb")
+
+// -- Case sensitive --
+//@workspacesymbolcasesensitive("main.main")
+//@workspacesymbolcasesensitive("p.Message")
+//@workspacesymbolcasesensitive("main.myvar")
+//@workspacesymbolcasesensitive("main.myType")
+//@workspacesymbolcasesensitive("main.myType.Blahblah")
+//@workspacesymbolcasesensitive("main.myStruct")
+//@workspacesymbolcasesensitive("main.myStruct.myStructField")
+//@workspacesymbolcasesensitive("main.myInterface")
+//@workspacesymbolcasesensitive("main.myInterface.DoSomeCoolStuff")
+//@workspacesymbolcasesensitive("main.embed.myStruct")
+//@workspacesymbolcasesensitive("main.embed.nestedStruct.nestedStruct2.int")
+//@workspacesymbolcasesensitive("main.embed.nestedInterface.myInterface")
+//@workspacesymbolcasesensitive("main.embed.nestedInterface.nestedMethod")
+//@workspacesymbolcasesensitive("dunk")
+//@workspacesymbolcasesensitive("Dunk")
+
+// -- Standard --
+//@workspacesymbol("")
+//@workspacesymbol("randomgophervar")
diff --git a/internal/lsp/testdata/workspacesymbol/query.go.golden b/internal/lsp/testdata/workspacesymbol/query.go.golden
new file mode 100644
index 0000000..832f86d
--- /dev/null
+++ b/internal/lsp/testdata/workspacesymbol/query.go.golden
@@ -0,0 +1,83 @@
+-- workspace_symbol-caseinsensitive- --
+
+
+-- workspace_symbol-caseinsensitive-randomgophervar --
+workspacesymbol/a/a.go:3:5-26 a.RandomGopherVariableA Variable
+workspacesymbol/b/b.go:3:5-26 b.RandomGopherVariableB Variable
+
+-- workspace_symbol-casesensitive-Dunk --
+workspacesymbol/main.go:45:6-10 main.Dunk Function
+
+-- workspace_symbol-casesensitive-dunk --
+workspacesymbol/main.go:47:6-10 main.dunk Function
+
+-- workspace_symbol-casesensitive-main.embed.myStruct --
+workspacesymbol/main.go:29:2-10 main.embed.myStruct Field
+
+-- workspace_symbol-casesensitive-main.embed.nestedInterface.myInterface --
+workspacesymbol/main.go:40:3-14 main.embed.nestedInterface.myInterface Interface
+
+-- workspace_symbol-casesensitive-main.embed.nestedInterface.nestedMethod --
+workspacesymbol/main.go:41:3-15 main.embed.nestedInterface.nestedMethod Method
+
+-- workspace_symbol-casesensitive-main.embed.nestedStruct.nestedStruct2.int --
+workspacesymbol/main.go:35:4-7 main.embed.nestedStruct.nestedStruct2.int Field
+
+-- workspace_symbol-casesensitive-main.main --
+workspacesymbol/main.go:8:6-10 main.main Function
+
+-- workspace_symbol-casesensitive-main.myInterface --
+workspacesymbol/main.go:24:6-17 main.myInterface Interface
+workspacesymbol/main.go:25:2-17 main.myInterface.DoSomeCoolStuff Method
+
+-- workspace_symbol-casesensitive-main.myInterface.DoSomeCoolStuff --
+workspacesymbol/main.go:25:2-17 main.myInterface.DoSomeCoolStuff Method
+
+-- workspace_symbol-casesensitive-main.myStruct --
+workspacesymbol/main.go:20:6-14 main.myStruct Struct
+workspacesymbol/main.go:21:2-15 main.myStruct.myStructField Field
+
+-- workspace_symbol-casesensitive-main.myStruct.myStructField --
+workspacesymbol/main.go:21:2-15 main.myStruct.myStructField Field
+
+-- workspace_symbol-casesensitive-main.myType --
+workspacesymbol/main.go:14:6-12 main.myType String
+workspacesymbol/main.go:18:18-26 main.myType.Blahblah Method
+
+-- workspace_symbol-casesensitive-main.myType.Blahblah --
+workspacesymbol/main.go:18:18-26 main.myType.Blahblah Method
+
+-- workspace_symbol-casesensitive-main.myvar --
+workspacesymbol/main.go:12:5-10 main.myvar Variable
+
+-- workspace_symbol-casesensitive-p.Message --
+workspacesymbol/p/p.go:3:7-14 p.Message Constant
+
+-- workspace_symbol-fuzzy-randoma --
+workspacesymbol/a/a.go:3:5-26 a.RandomGopherVariableA Variable
+workspacesymbol/a/a.go:5:7-28 a.RandomGopherConstantA Constant
+workspacesymbol/a/a.go:8:2-24 a.randomgopherinvariable Constant
+workspacesymbol/a/a_test.go:3:5-30 a.RandomGopherTestVariableA Variable
+workspacesymbol/a/a_x_test.go:3:5-31 a_test.RandomGopherXTestVariableA Variable
+workspacesymbol/b/b.go:3:5-26 b.RandomGopherVariableB Variable
+workspacesymbol/b/b.go:6:2-5 b.RandomGopherStructB.Bar Field
+
+-- workspace_symbol-fuzzy-randomb --
+workspacesymbol/a/a.go:3:5-26 a.RandomGopherVariableA Variable
+workspacesymbol/a/a.go:8:2-24 a.randomgopherinvariable Constant
+workspacesymbol/a/a_test.go:3:5-30 a.RandomGopherTestVariableA Variable
+workspacesymbol/a/a_x_test.go:3:5-31 a_test.RandomGopherXTestVariableA Variable
+workspacesymbol/b/b.go:3:5-26 b.RandomGopherVariableB Variable
+workspacesymbol/b/b.go:5:6-25 b.RandomGopherStructB Struct
+workspacesymbol/b/b.go:6:2-5 b.RandomGopherStructB.Bar Field
+
+-- workspace_symbol-fuzzy-rgop --
+workspacesymbol/a/a.go:3:5-26 a.RandomGopherVariableA Variable
+workspacesymbol/a/a.go:5:7-28 a.RandomGopherConstantA Constant
+workspacesymbol/a/a.go:8:2-24 a.randomgopherinvariable Constant
+workspacesymbol/a/a_test.go:3:5-30 a.RandomGopherTestVariableA Variable
+workspacesymbol/a/a_x_test.go:3:5-31 a_test.RandomGopherXTestVariableA Variable
+workspacesymbol/b/b.go:3:5-26 b.RandomGopherVariableB Variable
+workspacesymbol/b/b.go:5:6-25 b.RandomGopherStructB Struct
+workspacesymbol/b/b.go:6:2-5 b.RandomGopherStructB.Bar Field
+
diff --git a/internal/lsp/testdata/workspacesymbol/randomgophervar.golden b/internal/lsp/testdata/workspacesymbol/randomgophervar.golden
deleted file mode 100644
index 5f559b0..0000000
--- a/internal/lsp/testdata/workspacesymbol/randomgophervar.golden
+++ /dev/null
@@ -1,3 +0,0 @@
--- workspace_symbol --
-workspacesymbol/a/a.go:3:5-26 a.RandomGopherVariableA Variable
-workspacesymbol/b/b.go:3:5-26 b.RandomGopherVariableB Variable
diff --git a/internal/lsp/testdata/workspacesymbol/workspacesymbol.go b/internal/lsp/testdata/workspacesymbol/workspacesymbol.go
deleted file mode 100644
index bffb1dd..0000000
--- a/internal/lsp/testdata/workspacesymbol/workspacesymbol.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package workspacesymbol
-
-/*@
-workspacesymbol("") // The result is 0 symbols due to the limit(golang.org/cl/220939).
-workspacesymbol("randomgophervar",
-	RandomGopherVariableA,
-	RandomGopherVariableB,
-)
-*/
diff --git a/internal/lsp/tests/normalizer.go b/internal/lsp/tests/normalizer.go
index ebe0ef4..77d9e66 100644
--- a/internal/lsp/tests/normalizer.go
+++ b/internal/lsp/tests/normalizer.go
@@ -109,8 +109,17 @@
 		b.WriteString(s[last:n.index])
 		// skip over the filename
 		last = n.index + len(n.path)
+
+		// Hack: In multi-module mode, we add a "testmodule/" prefix, so trim
+		// it from the fragment.
+		fragment := n.fragment
+		if strings.HasPrefix(fragment, "testmodule") {
+			split := strings.Split(filepath.ToSlash(fragment), "/")
+			fragment = filepath.FromSlash(strings.Join(split[1:], "/"))
+		}
+
 		// add in the fragment instead
-		b.WriteString(n.fragment)
+		b.WriteString(fragment)
 		// see what the next match for this path is
 		n.index = strings.Index(s[last:], n.path)
 		if n.index >= 0 {
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index 05bd5ee..be3fc35 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -70,44 +70,42 @@
 type Symbols map[span.URI][]protocol.DocumentSymbol
 type SymbolsChildren map[string][]protocol.DocumentSymbol
 type SymbolInformation map[span.Span]protocol.SymbolInformation
-type WorkspaceSymbols map[string][]protocol.SymbolInformation
+type WorkspaceSymbols map[WorkspaceSymbolsTestType]map[span.URI][]string
 type Signatures map[span.Span]*protocol.SignatureHelp
 type Links map[span.URI][]Link
 
 type Data struct {
-	Config                        packages.Config
-	Exported                      *packagestest.Exported
-	CallHierarchy                 CallHierarchy
-	CodeLens                      CodeLens
-	Diagnostics                   Diagnostics
-	CompletionItems               CompletionItems
-	Completions                   Completions
-	CompletionSnippets            CompletionSnippets
-	UnimportedCompletions         UnimportedCompletions
-	DeepCompletions               DeepCompletions
-	FuzzyCompletions              FuzzyCompletions
-	CaseSensitiveCompletions      CaseSensitiveCompletions
-	RankCompletions               RankCompletions
-	FoldingRanges                 FoldingRanges
-	Formats                       Formats
-	Imports                       Imports
-	SemanticTokens                SemanticTokens
-	SuggestedFixes                SuggestedFixes
-	FunctionExtractions           FunctionExtractions
-	Definitions                   Definitions
-	Implementations               Implementations
-	Highlights                    Highlights
-	References                    References
-	Renames                       Renames
-	PrepareRenames                PrepareRenames
-	Symbols                       Symbols
-	symbolsChildren               SymbolsChildren
-	symbolInformation             SymbolInformation
-	WorkspaceSymbols              WorkspaceSymbols
-	FuzzyWorkspaceSymbols         WorkspaceSymbols
-	CaseSensitiveWorkspaceSymbols WorkspaceSymbols
-	Signatures                    Signatures
-	Links                         Links
+	Config                   packages.Config
+	Exported                 *packagestest.Exported
+	CallHierarchy            CallHierarchy
+	CodeLens                 CodeLens
+	Diagnostics              Diagnostics
+	CompletionItems          CompletionItems
+	Completions              Completions
+	CompletionSnippets       CompletionSnippets
+	UnimportedCompletions    UnimportedCompletions
+	DeepCompletions          DeepCompletions
+	FuzzyCompletions         FuzzyCompletions
+	CaseSensitiveCompletions CaseSensitiveCompletions
+	RankCompletions          RankCompletions
+	FoldingRanges            FoldingRanges
+	Formats                  Formats
+	Imports                  Imports
+	SemanticTokens           SemanticTokens
+	SuggestedFixes           SuggestedFixes
+	FunctionExtractions      FunctionExtractions
+	Definitions              Definitions
+	Implementations          Implementations
+	Highlights               Highlights
+	References               References
+	Renames                  Renames
+	PrepareRenames           PrepareRenames
+	Symbols                  Symbols
+	symbolsChildren          SymbolsChildren
+	symbolInformation        SymbolInformation
+	WorkspaceSymbols         WorkspaceSymbols
+	Signatures               Signatures
+	Links                    Links
 
 	t         testing.TB
 	fragments map[string]string
@@ -145,9 +143,7 @@
 	Rename(*testing.T, span.Span, string)
 	PrepareRename(*testing.T, span.Span, *source.PrepareItem)
 	Symbols(*testing.T, span.URI, []protocol.DocumentSymbol)
-	WorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{})
-	FuzzyWorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{})
-	CaseSensitiveWorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{})
+	WorkspaceSymbols(*testing.T, span.URI, string, WorkspaceSymbolsTestType)
 	SignatureHelp(*testing.T, span.Span, *protocol.SignatureHelp)
 	Link(*testing.T, span.URI, []Link)
 }
@@ -270,33 +266,31 @@
 	t.Helper()
 
 	datum := &Data{
-		CallHierarchy:                 make(CallHierarchy),
-		CodeLens:                      make(CodeLens),
-		Diagnostics:                   make(Diagnostics),
-		CompletionItems:               make(CompletionItems),
-		Completions:                   make(Completions),
-		CompletionSnippets:            make(CompletionSnippets),
-		UnimportedCompletions:         make(UnimportedCompletions),
-		DeepCompletions:               make(DeepCompletions),
-		FuzzyCompletions:              make(FuzzyCompletions),
-		RankCompletions:               make(RankCompletions),
-		CaseSensitiveCompletions:      make(CaseSensitiveCompletions),
-		Definitions:                   make(Definitions),
-		Implementations:               make(Implementations),
-		Highlights:                    make(Highlights),
-		References:                    make(References),
-		Renames:                       make(Renames),
-		PrepareRenames:                make(PrepareRenames),
-		SuggestedFixes:                make(SuggestedFixes),
-		FunctionExtractions:           make(FunctionExtractions),
-		Symbols:                       make(Symbols),
-		symbolsChildren:               make(SymbolsChildren),
-		symbolInformation:             make(SymbolInformation),
-		WorkspaceSymbols:              make(WorkspaceSymbols),
-		FuzzyWorkspaceSymbols:         make(WorkspaceSymbols),
-		CaseSensitiveWorkspaceSymbols: make(WorkspaceSymbols),
-		Signatures:                    make(Signatures),
-		Links:                         make(Links),
+		CallHierarchy:            make(CallHierarchy),
+		CodeLens:                 make(CodeLens),
+		Diagnostics:              make(Diagnostics),
+		CompletionItems:          make(CompletionItems),
+		Completions:              make(Completions),
+		CompletionSnippets:       make(CompletionSnippets),
+		UnimportedCompletions:    make(UnimportedCompletions),
+		DeepCompletions:          make(DeepCompletions),
+		FuzzyCompletions:         make(FuzzyCompletions),
+		RankCompletions:          make(RankCompletions),
+		CaseSensitiveCompletions: make(CaseSensitiveCompletions),
+		Definitions:              make(Definitions),
+		Implementations:          make(Implementations),
+		Highlights:               make(Highlights),
+		References:               make(References),
+		Renames:                  make(Renames),
+		PrepareRenames:           make(PrepareRenames),
+		SuggestedFixes:           make(SuggestedFixes),
+		FunctionExtractions:      make(FunctionExtractions),
+		Symbols:                  make(Symbols),
+		symbolsChildren:          make(SymbolsChildren),
+		symbolInformation:        make(SymbolInformation),
+		WorkspaceSymbols:         make(WorkspaceSymbols),
+		Signatures:               make(Signatures),
+		Links:                    make(Links),
 
 		t:         t,
 		dir:       dir,
@@ -503,28 +497,6 @@
 		}
 	}
 
-	eachWorkspaceSymbols := func(t *testing.T, cases map[string][]protocol.SymbolInformation, test func(*testing.T, string, []protocol.SymbolInformation, map[string]struct{})) {
-		t.Helper()
-
-		for query, expectedSymbols := range cases {
-			name := query
-			if name == "" {
-				name = "EmptyQuery"
-			}
-			t.Run(name, func(t *testing.T) {
-				t.Helper()
-				dirs := make(map[string]struct{})
-				for _, si := range expectedSymbols {
-					d := filepath.Dir(si.Location.URI.SpanURI().Filename())
-					if _, ok := dirs[d]; !ok {
-						dirs[d] = struct{}{}
-					}
-				}
-				test(t, query, expectedSymbols, dirs)
-			})
-		}
-	}
-
 	t.Run("CallHierarchy", func(t *testing.T) {
 		t.Helper()
 		for spn, callHierarchyResult := range data.CallHierarchy {
@@ -758,17 +730,26 @@
 
 	t.Run("WorkspaceSymbols", func(t *testing.T) {
 		t.Helper()
-		eachWorkspaceSymbols(t, data.WorkspaceSymbols, tests.WorkspaceSymbols)
-	})
 
-	t.Run("FuzzyWorkspaceSymbols", func(t *testing.T) {
-		t.Helper()
-		eachWorkspaceSymbols(t, data.FuzzyWorkspaceSymbols, tests.FuzzyWorkspaceSymbols)
-	})
+		for _, typ := range []WorkspaceSymbolsTestType{
+			WorkspaceSymbolsDefault,
+			WorkspaceSymbolsCaseSensitive,
+			WorkspaceSymbolsFuzzy,
+		} {
+			for uri, cases := range data.WorkspaceSymbols[typ] {
+				for _, query := range cases {
+					name := query
+					if name == "" {
+						name = "EmptyQuery"
+					}
+					t.Run(name, func(t *testing.T) {
+						t.Helper()
+						tests.WorkspaceSymbols(t, uri, query, typ)
+					})
+				}
+			}
+		}
 
-	t.Run("CaseSensitiveWorkspaceSymbols", func(t *testing.T) {
-		t.Helper()
-		eachWorkspaceSymbols(t, data.CaseSensitiveWorkspaceSymbols, tests.CaseSensitiveWorkspaceSymbols)
 	})
 
 	t.Run("SignatureHelp", func(t *testing.T) {
@@ -857,6 +838,15 @@
 		return count
 	}
 
+	countWorkspaceSymbols := func(c map[WorkspaceSymbolsTestType]map[span.URI][]string) (count int) {
+		for _, typs := range c {
+			for _, queries := range typs {
+				count += len(queries)
+			}
+		}
+		return count
+	}
+
 	fmt.Fprintf(buf, "CallHierarchyCount = %v\n", len(data.CallHierarchy))
 	fmt.Fprintf(buf, "CodeLensCount = %v\n", countCodeLens(data.CodeLens))
 	fmt.Fprintf(buf, "CompletionsCount = %v\n", countCompletions(data.Completions))
@@ -880,9 +870,7 @@
 	fmt.Fprintf(buf, "RenamesCount = %v\n", len(data.Renames))
 	fmt.Fprintf(buf, "PrepareRenamesCount = %v\n", len(data.PrepareRenames))
 	fmt.Fprintf(buf, "SymbolsCount = %v\n", len(data.Symbols))
-	fmt.Fprintf(buf, "WorkspaceSymbolsCount = %v\n", len(data.WorkspaceSymbols))
-	fmt.Fprintf(buf, "FuzzyWorkspaceSymbolsCount = %v\n", len(data.FuzzyWorkspaceSymbols))
-	fmt.Fprintf(buf, "CaseSensitiveWorkspaceSymbolsCount = %v\n", len(data.CaseSensitiveWorkspaceSymbols))
+	fmt.Fprintf(buf, "WorkspaceSymbolsCount = %v\n", countWorkspaceSymbols(data.WorkspaceSymbols))
 	fmt.Fprintf(buf, "SignaturesCount = %v\n", len(data.Signatures))
 	fmt.Fprintf(buf, "LinksCount = %v\n", linksCount)
 	fmt.Fprintf(buf, "ImplementationsCount = %v\n", len(data.Implementations))
@@ -929,12 +917,8 @@
 		if !*UpdateGolden {
 			data.t.Fatalf("could not find golden file %v: %v", fragment, tag)
 		}
-		var subdir string
-		if fragment != summaryFile {
-			subdir = "primarymod"
-		}
 		golden = &Golden{
-			Filename: filepath.Join(data.dir, subdir, fragment+goldenFileSuffix),
+			Filename: filepath.Join(data.dir, fragment+goldenFileSuffix),
 			Archive:  &txtar.Archive{},
 			Modified: true,
 		}
@@ -1261,29 +1245,14 @@
 	data.symbolInformation[spn] = si
 }
 
-func (data *Data) collectWorkspaceSymbols(typ WorkspaceSymbolsTestType) func(string, []span.Span) {
-	switch typ {
-	case WorkspaceSymbolsFuzzy:
-		return func(query string, targets []span.Span) {
-			data.FuzzyWorkspaceSymbols[query] = make([]protocol.SymbolInformation, 0, len(targets))
-			for _, target := range targets {
-				data.FuzzyWorkspaceSymbols[query] = append(data.FuzzyWorkspaceSymbols[query], data.symbolInformation[target])
-			}
+func (data *Data) collectWorkspaceSymbols(typ WorkspaceSymbolsTestType) func(*expect.Note, string) {
+	return func(note *expect.Note, query string) {
+		if data.WorkspaceSymbols[typ] == nil {
+			data.WorkspaceSymbols[typ] = make(map[span.URI][]string)
 		}
-	case WorkspaceSymbolsCaseSensitive:
-		return func(query string, targets []span.Span) {
-			data.CaseSensitiveWorkspaceSymbols[query] = make([]protocol.SymbolInformation, 0, len(targets))
-			for _, target := range targets {
-				data.CaseSensitiveWorkspaceSymbols[query] = append(data.CaseSensitiveWorkspaceSymbols[query], data.symbolInformation[target])
-			}
-		}
-	default:
-		return func(query string, targets []span.Span) {
-			data.WorkspaceSymbols[query] = make([]protocol.SymbolInformation, 0, len(targets))
-			for _, target := range targets {
-				data.WorkspaceSymbols[query] = append(data.WorkspaceSymbols[query], data.symbolInformation[target])
-			}
-		}
+		pos := data.Exported.ExpectFileSet.Position(note.Pos)
+		uri := span.URIFromPath(pos.Filename)
+		data.WorkspaceSymbols[typ][uri] = append(data.WorkspaceSymbols[typ][uri], query)
 	}
 }
 
diff --git a/internal/lsp/tests/util.go b/internal/lsp/tests/util.go
index f002683..0c050d0 100644
--- a/internal/lsp/tests/util.go
+++ b/internal/lsp/tests/util.go
@@ -6,6 +6,7 @@
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"go/token"
 	"path/filepath"
@@ -108,62 +109,6 @@
 	return msg.String()
 }
 
-// FilterWorkspaceSymbols filters to got contained in the given dirs.
-func FilterWorkspaceSymbols(got []protocol.SymbolInformation, dirs map[string]struct{}) []protocol.SymbolInformation {
-	var result []protocol.SymbolInformation
-	for _, si := range got {
-		if _, ok := dirs[filepath.Dir(si.Location.URI.SpanURI().Filename())]; ok {
-			result = append(result, si)
-		}
-	}
-	return result
-}
-
-// DiffWorkspaceSymbols prints the diff between expected and actual workspace
-// symbols test results.
-func DiffWorkspaceSymbols(want, got []protocol.SymbolInformation) string {
-	sort.Slice(want, func(i, j int) bool { return fmt.Sprintf("%v", want[i]) < fmt.Sprintf("%v", want[j]) })
-	sort.Slice(got, func(i, j int) bool { return fmt.Sprintf("%v", got[i]) < fmt.Sprintf("%v", got[j]) })
-	if len(got) != len(want) {
-		return summarizeWorkspaceSymbols(-1, want, got, "different lengths got %v want %v", len(got), len(want))
-	}
-	for i, w := range want {
-		g := got[i]
-		if w.Name != g.Name {
-			return summarizeWorkspaceSymbols(i, want, got, "incorrect name got %v want %v", g.Name, w.Name)
-		}
-		if w.Kind != g.Kind {
-			return summarizeWorkspaceSymbols(i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind)
-		}
-		if w.Location.URI != g.Location.URI {
-			return summarizeWorkspaceSymbols(i, want, got, "incorrect uri got %v want %v", g.Location.URI, w.Location.URI)
-		}
-		if protocol.CompareRange(w.Location.Range, g.Location.Range) != 0 {
-			return summarizeWorkspaceSymbols(i, want, got, "incorrect range got %v want %v", g.Location.Range, w.Location.Range)
-		}
-	}
-	return ""
-}
-
-func summarizeWorkspaceSymbols(i int, want, got []protocol.SymbolInformation, reason string, args ...interface{}) string {
-	msg := &bytes.Buffer{}
-	fmt.Fprint(msg, "workspace symbols failed")
-	if i >= 0 {
-		fmt.Fprintf(msg, " at %d", i)
-	}
-	fmt.Fprint(msg, " because of ")
-	fmt.Fprintf(msg, reason, args...)
-	fmt.Fprint(msg, ":\nexpected:\n")
-	for _, s := range want {
-		fmt.Fprintf(msg, "  %v %v %v:%v\n", s.Name, s.Kind, s.Location.URI, s.Location.Range)
-	}
-	fmt.Fprintf(msg, "got:\n")
-	for _, s := range got {
-		fmt.Fprintf(msg, "  %v %v %v:%v\n", s.Name, s.Kind, s.Location.URI, s.Location.Range)
-	}
-	return msg.String()
-}
-
 // DiffDiagnostics prints the diff between expected and actual diagnostics test
 // results.
 func DiffDiagnostics(uri span.URI, want, got []*source.Diagnostic) string {
@@ -544,6 +489,40 @@
 	}
 }
 
+func WorkspaceSymbolsString(ctx context.Context, data *Data, queryURI span.URI, symbols []protocol.SymbolInformation) (string, error) {
+	queryDir := filepath.Dir(queryURI.Filename())
+	var filtered []string
+	for _, s := range symbols {
+		uri := s.Location.URI.SpanURI()
+		dir := filepath.Dir(uri.Filename())
+		if !source.InDir(queryDir, dir) { // assume queries always issue from higher directories
+			continue
+		}
+		m, err := data.Mapper(uri)
+		if err != nil {
+			return "", err
+		}
+		spn, err := m.Span(s.Location)
+		if err != nil {
+			return "", err
+		}
+		filtered = append(filtered, fmt.Sprintf("%s %s %s", spn, s.Name, s.Kind))
+	}
+	sort.Strings(filtered)
+	return strings.Join(filtered, "\n") + "\n", nil
+}
+
+func WorkspaceSymbolsTestTypeToMatcher(typ WorkspaceSymbolsTestType) source.SymbolMatcher {
+	switch typ {
+	case WorkspaceSymbolsFuzzy:
+		return source.SymbolFuzzy
+	case WorkspaceSymbolsCaseSensitive:
+		return source.SymbolCaseSensitive
+	default:
+		return source.SymbolCaseInsensitive
+	}
+}
+
 func Diff(want, got string) string {
 	if want == got {
 		return ""