internal/lsp: precompute workspace symbols

Coupling workspace symbols to package checking means that they do not
function when the workspace is contracted, and also forces us to do
duplicate work traversing file declarations.

This CL changes the workspace symbol implementation to precompute
symbols based only on syntactic information, allowing them to function
in degraded workspace mode, improving their performance, and laying the
groundwork for more significant performance improvement later on.

There is some loss of precision where we can't determine the kind of a
symbol from syntactic information alone, but this is minor: we fall back
on 'Class' if we can't determine whether a type definition is a basic
type, struct, or interface.

Benchmark ("test" in x/tools): 56ms->40ms
Benchmark ("test" in kuberneted): 874ms->799ms

Change-Id: Ic48df29b387bf029dd374d7d09720746bc27ae5e
Reviewed-on: https://go-review.googlesource.com/c/tools/+/338692
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index 1baa3e5..b13e4c0 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -52,6 +52,16 @@
 	isIntermediateTestVariant bool
 }
 
+// Name implements the source.Metadata interface.
+func (m *metadata) Name() string {
+	return string(m.name)
+}
+
+// PkgPath implements the source.Metadata interface.
+func (m *metadata) PkgPath() string {
+	return string(m.pkgPath)
+}
+
 // load calls packages.Load for the given scopes, updating package metadata,
 // import graph, and mapped files with the result.
 func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interface{}) (err error) {
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 7e78811..487270b 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -234,6 +234,7 @@
 		metadata:          make(map[packageID]*knownMetadata),
 		files:             make(map[span.URI]source.VersionedFileHandle),
 		goFiles:           make(map[parseKey]*parseGoHandle),
+		symbols:           make(map[span.URI]*symbolHandle),
 		importedBy:        make(map[packageID][]packageID),
 		actions:           make(map[actionKey]*actionHandle),
 		workspacePackages: make(map[packageID]packagePath),
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 6ea3aa5..8978dfc 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -85,6 +85,9 @@
 	// goFiles maps a parseKey to its parseGoHandle.
 	goFiles map[parseKey]*parseGoHandle
 
+	// TODO(rfindley): consider merging this with files to reduce burden on clone.
+	symbols map[span.URI]*symbolHandle
+
 	// packages maps a packageKey to a set of packageHandles to which that file belongs.
 	// It may be invalidated when a file's content changes.
 	packages map[packageKey]*packageHandle
@@ -523,32 +526,9 @@
 	if fh.Kind() != source.Go {
 		return nil, fmt.Errorf("no packages for non-Go file %s", uri)
 	}
-	knownIDs := s.getIDsForURI(uri)
-	reload := len(knownIDs) == 0
-	for _, id := range knownIDs {
-		// Reload package metadata if any of the metadata has missing
-		// dependencies, in case something has changed since the last time we
-		// reloaded it.
-		if s.noValidMetadataForID(id) {
-			reload = true
-			break
-		}
-		// TODO(golang/go#36918): Previously, we would reload any package with
-		// missing dependencies. This is expensive and results in too many
-		// calls to packages.Load. Determine what we should do instead.
-	}
-	if reload {
-		err = s.load(ctx, false, fileURI(uri))
-
-		if !s.useInvalidMetadata() && err != nil {
-			return nil, err
-		}
-		// We've tried to reload and there are still no known IDs for the URI.
-		// Return the load error, if there was one.
-		knownIDs = s.getIDsForURI(uri)
-		if len(knownIDs) == 0 {
-			return nil, err
-		}
+	knownIDs, err := s.getOrLoadIDsForURI(ctx, uri)
+	if err != nil {
+		return nil, err
 	}
 
 	var phs []*packageHandle
@@ -583,6 +563,37 @@
 	return phs, nil
 }
 
+func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]packageID, error) {
+	knownIDs := s.getIDsForURI(uri)
+	reload := len(knownIDs) == 0
+	for _, id := range knownIDs {
+		// Reload package metadata if any of the metadata has missing
+		// dependencies, in case something has changed since the last time we
+		// reloaded it.
+		if s.noValidMetadataForID(id) {
+			reload = true
+			break
+		}
+		// TODO(golang/go#36918): Previously, we would reload any package with
+		// missing dependencies. This is expensive and results in too many
+		// calls to packages.Load. Determine what we should do instead.
+	}
+	if reload {
+		err := s.load(ctx, false, fileURI(uri))
+
+		if !s.useInvalidMetadata() && err != nil {
+			return nil, err
+		}
+		// We've tried to reload and there are still no known IDs for the URI.
+		// Return the load error, if there was one.
+		knownIDs = s.getIDsForURI(uri)
+		if len(knownIDs) == 0 {
+			return nil, err
+		}
+	}
+	return knownIDs, nil
+}
+
 // Only use invalid metadata for Go versions >= 1.13. Go 1.12 and below has
 // issues with overlays that will cause confusing error messages if we reuse
 // old metadata.
@@ -960,6 +971,33 @@
 	return phs, nil
 }
 
+func (s *snapshot) Symbols(ctx context.Context) (map[span.URI][]source.Symbol, error) {
+	result := make(map[span.URI][]source.Symbol)
+	for uri, f := range s.files {
+		sh := s.buildSymbolHandle(ctx, f)
+		v, err := sh.handle.Get(ctx, s.generation, s)
+		if err != nil {
+			return nil, err
+		}
+		data := v.(*symbolData)
+		result[uri] = data.symbols
+	}
+	return result, nil
+}
+
+func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]source.Metadata, error) {
+	knownIDs, err := s.getOrLoadIDsForURI(ctx, uri)
+	if err != nil {
+		return nil, err
+	}
+	var mds []source.Metadata
+	for _, id := range knownIDs {
+		md := s.getMetadata(id)
+		mds = append(mds, md)
+	}
+	return mds, nil
+}
+
 func (s *snapshot) KnownPackages(ctx context.Context) ([]source.Package, error) {
 	if err := s.awaitLoaded(ctx); err != nil {
 		return nil, err
@@ -1044,6 +1082,26 @@
 	return s.packages[key]
 }
 
+func (s *snapshot) getSymbolHandle(uri span.URI) *symbolHandle {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	return s.symbols[uri]
+}
+
+func (s *snapshot) addSymbolHandle(sh *symbolHandle) *symbolHandle {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	uri := sh.fh.URI()
+	// If the package handle has already been cached,
+	// return the cached handle instead of overriding it.
+	if sh, ok := s.symbols[uri]; ok {
+		return sh
+	}
+	s.symbols[uri] = sh
+	return sh
+}
 func (s *snapshot) getActionHandle(id packageID, m source.ParseMode, a *analysis.Analyzer) *actionHandle {
 	s.mu.Lock()
 	defer s.mu.Unlock()
@@ -1633,6 +1691,7 @@
 		actions:           make(map[actionKey]*actionHandle, len(s.actions)),
 		files:             make(map[span.URI]source.VersionedFileHandle, len(s.files)),
 		goFiles:           make(map[parseKey]*parseGoHandle, len(s.goFiles)),
+		symbols:           make(map[span.URI]*symbolHandle, len(s.symbols)),
 		workspacePackages: make(map[packageID]packagePath, len(s.workspacePackages)),
 		unloadableFiles:   make(map[span.URI]struct{}, len(s.unloadableFiles)),
 		parseModHandles:   make(map[span.URI]*parseModHandle, len(s.parseModHandles)),
@@ -1651,6 +1710,16 @@
 	for k, v := range s.files {
 		result.files[k] = v
 	}
+	for k, v := range s.symbols {
+		if change, ok := changes[k]; ok {
+			if change.exists {
+				result.symbols[k] = result.buildSymbolHandle(ctx, change.fileHandle)
+			}
+			continue
+		}
+		newGen.Inherit(v.handle)
+		result.symbols[k] = v
+	}
 
 	// Copy the set of unloadable files.
 	for k, v := range s.unloadableFiles {
diff --git a/internal/lsp/cache/symbols.go b/internal/lsp/cache/symbols.go
new file mode 100644
index 0000000..d1ecf2a
--- /dev/null
+++ b/internal/lsp/cache/symbols.go
@@ -0,0 +1,211 @@
+// Copyright 2021 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 cache
+
+import (
+	"context"
+	"go/ast"
+	"go/token"
+	"go/types"
+	"strings"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/memoize"
+	"golang.org/x/tools/internal/span"
+)
+
+type symbolHandle struct {
+	handle *memoize.Handle
+
+	fh source.FileHandle
+
+	// key is the hashed key for the package.
+	key symbolHandleKey
+}
+
+// symbolData contains the data produced by extracting symbols from a file.
+type symbolData struct {
+	symbols []source.Symbol
+	err     error
+}
+
+type symbolHandleKey string
+
+func (s *snapshot) buildSymbolHandle(ctx context.Context, fh source.FileHandle) *symbolHandle {
+	if h := s.getSymbolHandle(fh.URI()); h != nil {
+		return h
+	}
+	key := symbolHandleKey(fh.FileIdentity().Hash)
+	h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
+		snapshot := arg.(*snapshot)
+		data := &symbolData{}
+		data.symbols, data.err = symbolize(ctx, snapshot, fh)
+		return data
+	}, nil)
+
+	sh := &symbolHandle{
+		handle: h,
+		fh:     fh,
+		key:    key,
+	}
+	return s.addSymbolHandle(sh)
+}
+
+// symbolize extracts symbols from a file. It does not parse the file through the cache.
+func symbolize(ctx context.Context, snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, error) {
+	var w symbolWalker
+	fset := token.NewFileSet() // don't use snapshot.FileSet, as that would needlessly leak memory.
+	data := parseGo(ctx, fset, fh, source.ParseFull)
+	if data.parsed != nil && data.parsed.File != nil {
+		w.curFile = data.parsed
+		w.curURI = protocol.URIFromSpanURI(data.parsed.URI)
+		w.fileDecls(data.parsed.File.Decls)
+	}
+	return w.symbols, w.firstError
+}
+
+type symbolWalker struct {
+	curFile    *source.ParsedGoFile
+	pkgName    string
+	curURI     protocol.DocumentURI
+	symbols    []source.Symbol
+	firstError error
+}
+
+func (w *symbolWalker) atNode(node ast.Node, name string, kind protocol.SymbolKind, path ...*ast.Ident) {
+	var b strings.Builder
+	for _, ident := range path {
+		if ident != nil {
+			b.WriteString(ident.Name)
+			b.WriteString(".")
+		}
+	}
+	b.WriteString(name)
+
+	rng, err := fileRange(w.curFile, node.Pos(), node.End())
+	if err != nil {
+		w.error(err)
+		return
+	}
+	sym := source.Symbol{
+		Name:  b.String(),
+		Kind:  kind,
+		Range: rng,
+	}
+	w.symbols = append(w.symbols, sym)
+}
+
+func (w *symbolWalker) error(err error) {
+	if err != nil && w.firstError == nil {
+		w.firstError = err
+	}
+}
+
+func fileRange(pgf *source.ParsedGoFile, start, end token.Pos) (protocol.Range, error) {
+	s, err := span.FileSpan(pgf.Tok, pgf.Mapper.Converter, start, end)
+	if err != nil {
+		return protocol.Range{}, nil
+	}
+	return pgf.Mapper.Range(s)
+}
+
+func (w *symbolWalker) fileDecls(decls []ast.Decl) {
+	for _, decl := range decls {
+		switch decl := decl.(type) {
+		case *ast.FuncDecl:
+			kind := protocol.Function
+			var recv *ast.Ident
+			if decl.Recv.NumFields() > 0 {
+				kind = protocol.Method
+				recv = unpackRecv(decl.Recv.List[0].Type)
+			}
+			w.atNode(decl.Name, decl.Name.Name, kind, recv)
+		case *ast.GenDecl:
+			for _, spec := range decl.Specs {
+				switch spec := spec.(type) {
+				case *ast.TypeSpec:
+					kind := guessKind(spec)
+					w.atNode(spec.Name, spec.Name.Name, kind)
+					w.walkType(spec.Type, spec.Name)
+				case *ast.ValueSpec:
+					for _, name := range spec.Names {
+						kind := protocol.Variable
+						if decl.Tok == token.CONST {
+							kind = protocol.Constant
+						}
+						w.atNode(name, name.Name, kind)
+					}
+				}
+			}
+		}
+	}
+}
+
+func guessKind(spec *ast.TypeSpec) protocol.SymbolKind {
+	switch spec.Type.(type) {
+	case *ast.InterfaceType:
+		return protocol.Interface
+	case *ast.StructType:
+		return protocol.Struct
+	case *ast.FuncType:
+		return protocol.Function
+	}
+	return protocol.Class
+}
+
+func unpackRecv(rtyp ast.Expr) *ast.Ident {
+	// Extract the receiver identifier. Lifted from go/types/resolver.go
+L:
+	for {
+		switch t := rtyp.(type) {
+		case *ast.ParenExpr:
+			rtyp = t.X
+		case *ast.StarExpr:
+			rtyp = t.X
+		default:
+			break L
+		}
+	}
+	if name, _ := rtyp.(*ast.Ident); name != nil {
+		return name
+	}
+	return nil
+}
+
+// walkType processes symbols related to a type expression. path is path of
+// nested type identifiers to the type expression.
+func (w *symbolWalker) walkType(typ ast.Expr, path ...*ast.Ident) {
+	switch st := typ.(type) {
+	case *ast.StructType:
+		for _, field := range st.Fields.List {
+			w.walkField(field, protocol.Field, protocol.Field, path...)
+		}
+	case *ast.InterfaceType:
+		for _, field := range st.Methods.List {
+			w.walkField(field, protocol.Interface, protocol.Method, path...)
+		}
+	}
+}
+
+// walkField processes symbols related to the struct field or interface method.
+//
+// unnamedKind and namedKind are the symbol kinds if the field is resp. unnamed
+// or named. path is the path of nested identifiers containing the field.
+func (w *symbolWalker) walkField(field *ast.Field, unnamedKind, namedKind protocol.SymbolKind, path ...*ast.Ident) {
+	if len(field.Names) == 0 {
+		switch typ := field.Type.(type) {
+		case *ast.SelectorExpr:
+			// embedded qualified type
+			w.atNode(field, typ.Sel.Name, unnamedKind, path...)
+		default:
+			w.atNode(field, types.ExprString(field.Type), unnamedKind, path...)
+		}
+	}
+	for _, name := range field.Names {
+		w.atNode(name, name.Name, namedKind, path...)
+		w.walkType(field.Type, append(path, name)...)
+	}
+}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 9079ca5..e5acf49 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -162,6 +162,12 @@
 	// mode, this is just the reverse transitive closure of open packages.
 	ActivePackages(ctx context.Context) ([]Package, error)
 
+	// Symbols returns all symbols in the snapshot.
+	Symbols(ctx context.Context) (map[span.URI][]Symbol, error)
+
+	// Metadata returns package metadata associated with the given file URI.
+	MetadataForFile(ctx context.Context, uri span.URI) ([]Metadata, error)
+
 	// GetCriticalError returns any critical errors in the workspace.
 	GetCriticalError(ctx context.Context) *CriticalError
 
@@ -299,6 +305,15 @@
 	TidiedContent []byte
 }
 
+// Metadata represents package metadata retrieved from go/packages.
+type Metadata interface {
+	// Name is the package name.
+	Name() string
+
+	// PkgPath is the package path.
+	PkgPath() string
+}
+
 // Session represents a single connection from a client.
 // This is the level at which things like open files are maintained on behalf
 // of the client.
diff --git a/internal/lsp/source/workspace_symbol.go b/internal/lsp/source/workspace_symbol.go
index 76c8ded..92683e7 100644
--- a/internal/lsp/source/workspace_symbol.go
+++ b/internal/lsp/source/workspace_symbol.go
@@ -7,13 +7,10 @@
 import (
 	"context"
 	"fmt"
-	"go/ast"
-	"go/token"
 	"go/types"
 	"sort"
 	"strings"
 	"unicode"
-	"unicode/utf8"
 
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/lsp/fuzzy"
@@ -21,6 +18,15 @@
 	"golang.org/x/tools/internal/span"
 )
 
+// Symbol holds a precomputed symbol value. Note: we avoid using the
+// protocol.SymbolInformation struct here in order to reduce the size of each
+// symbol.
+type Symbol struct {
+	Name  string
+	Kind  protocol.SymbolKind
+	Range protocol.Range
+}
+
 // maxSymbols defines the maximum number of symbol results that should ever be
 // sent in response to a client.
 const maxSymbols = 100
@@ -63,19 +69,17 @@
 // []string{"myType.field"} or []string{"myType.", "field"}.
 //
 // See the comment for symbolCollector for more information.
-type symbolizer func(nameParts []string, pkg Package, m matcherFunc) (string, float64)
+type symbolizer func(name string, pkg Metadata, m matcherFunc) (string, float64)
 
-func fullyQualifiedSymbolMatch(nameParts []string, pkg Package, matcher matcherFunc) (string, float64) {
-	_, score := dynamicSymbolMatch(nameParts, pkg, matcher)
-	path := append([]string{pkg.PkgPath() + "."}, nameParts...)
+func fullyQualifiedSymbolMatch(name string, pkg Metadata, matcher matcherFunc) (string, float64) {
+	_, score := dynamicSymbolMatch(name, pkg, matcher)
 	if score > 0 {
-		return strings.Join(path, ""), score
+		return pkg.PkgPath() + "." + name, score
 	}
 	return "", 0
 }
 
-func dynamicSymbolMatch(nameParts []string, pkg Package, matcher matcherFunc) (string, float64) {
-	name := strings.Join(nameParts, "")
+func dynamicSymbolMatch(name string, pkg Metadata, matcher matcherFunc) (string, float64) {
 	var score float64
 
 	endsInPkgName := strings.HasSuffix(pkg.PkgPath(), pkg.Name())
@@ -122,9 +126,8 @@
 	return fullyQualified, score * 0.6
 }
 
-func packageSymbolMatch(components []string, pkg Package, matcher matcherFunc) (string, float64) {
-	path := append([]string{pkg.Name() + "."}, components...)
-	qualified := strings.Join(path, "")
+func packageSymbolMatch(name string, pkg Metadata, matcher matcherFunc) (string, float64) {
+	qualified := pkg.Name() + "." + name
 	if _, s := matcher(qualified); s > 0 {
 		return qualified, s
 	}
@@ -145,11 +148,8 @@
 	matcher    matcherFunc
 	symbolizer symbolizer
 
-	// current holds metadata for the package we are currently walking.
-	current *pkgView
-	curFile *ParsedGoFile
-
-	res [maxSymbols]symbolInformation
+	seen map[span.URI]bool
+	res  [maxSymbols]symbolInformation
 }
 
 func newSymbolCollector(matcher SymbolMatcher, style SymbolStyle, query string) *symbolCollector {
@@ -281,29 +281,137 @@
 	return first, score
 }
 
-// walk walks views, gathers symbols, and returns the results.
-func (sc *symbolCollector) walk(ctx context.Context, views []View) (_ []protocol.SymbolInformation, err error) {
-	toWalk, err := sc.collectPackages(ctx, views)
-	if err != nil {
-		return nil, err
+func (sc *symbolCollector) walk(ctx context.Context, views []View) ([]protocol.SymbolInformation, error) {
+
+	// Use the root view URIs for determining (lexically) whether a uri is in any
+	// open workspace.
+	var roots []string
+	for _, v := range views {
+		roots = append(roots, strings.TrimRight(string(v.Folder()), "/"))
 	}
-	// Make sure we only walk files once (we might see them more than once due to
-	// build constraints).
-	seen := make(map[span.URI]bool)
-	for _, pv := range toWalk {
-		sc.current = pv
-		for _, pgf := range pv.pkg.CompiledGoFiles() {
-			if seen[pgf.URI] {
+
+	for _, v := range views {
+		snapshot, release := v.Snapshot(ctx)
+		defer release()
+		psyms, err := snapshot.Symbols(ctx)
+		if err != nil {
+			return nil, err
+		}
+
+		for uri, syms := range psyms {
+			mds, err := snapshot.MetadataForFile(ctx, uri)
+			if err != nil {
+				return nil, err
+			}
+			if len(mds) == 0 {
+				// TODO: should use the bug reporting API
 				continue
 			}
-			seen[pgf.URI] = true
-			sc.curFile = pgf
-			sc.walkFilesDecls(pgf.File.Decls)
+			md := mds[0]
+			for _, sym := range syms {
+				symbol, score := sc.symbolizer(sym.Name, md, sc.matcher)
+
+				// Check if the score is too low before applying any downranking.
+				if sc.tooLow(score) {
+					continue
+				}
+
+				// Factors to apply to the match score for the purpose of downranking
+				// results.
+				//
+				// These numbers were crudely calibrated based on trial-and-error using a
+				// small number of sample queries. Adjust as necessary.
+				//
+				// All factors are multiplicative, meaning if more than one applies they are
+				// multiplied together.
+				const (
+					// nonWorkspaceFactor is applied to symbols outside of any active
+					// workspace. Developers are less likely to want to jump to code that they
+					// are not actively working on.
+					nonWorkspaceFactor = 0.5
+					// nonWorkspaceUnexportedFactor is applied to unexported symbols outside of
+					// any active workspace. Since one wouldn't usually jump to unexported
+					// symbols to understand a package API, they are particularly irrelevant.
+					nonWorkspaceUnexportedFactor = 0.5
+					// every field or method nesting level to access the field decreases
+					// the score by a factor of 1.0 - depth*depthFactor, up to a depth of
+					// 3.
+					depthFactor = 0.2
+				)
+
+				startWord := true
+				exported := true
+				depth := 0.0
+				for _, r := range sym.Name {
+					if startWord && !unicode.IsUpper(r) {
+						exported = false
+					}
+					if r == '.' {
+						startWord = true
+						depth++
+					} else {
+						startWord = false
+					}
+				}
+
+				inWorkspace := false
+				for _, root := range roots {
+					if strings.HasPrefix(string(uri), root) {
+						inWorkspace = true
+						break
+					}
+				}
+
+				// Apply downranking based on workspace position.
+				if !inWorkspace {
+					score *= nonWorkspaceFactor
+					if !exported {
+						score *= nonWorkspaceUnexportedFactor
+					}
+				}
+
+				// Apply downranking based on symbol depth.
+				if depth > 3 {
+					depth = 3
+				}
+				score *= 1.0 - depth*depthFactor
+
+				if sc.tooLow(score) {
+					continue
+				}
+
+				si := symbolInformation{
+					score:     score,
+					symbol:    symbol,
+					kind:      sym.Kind,
+					uri:       uri,
+					rng:       sym.Range,
+					container: md.PkgPath(),
+				}
+				sc.store(si)
+			}
 		}
 	}
 	return sc.results(), nil
 }
 
+func (sc *symbolCollector) store(si symbolInformation) {
+	if sc.tooLow(si.score) {
+		return
+	}
+	insertAt := sort.Search(len(sc.res), func(i int) bool {
+		return sc.res[i].score < si.score
+	})
+	if insertAt < len(sc.res)-1 {
+		copy(sc.res[insertAt+1:], sc.res[insertAt:len(sc.res)-1])
+	}
+	sc.res[insertAt] = si
+}
+
+func (sc *symbolCollector) tooLow(score float64) bool {
+	return score <= sc.res[len(sc.res)-1].score
+}
+
 func (sc *symbolCollector) results() []protocol.SymbolInformation {
 	var res []protocol.SymbolInformation
 	for _, si := range sc.res {
@@ -315,139 +423,6 @@
 	return res
 }
 
-// collectPackages gathers all known packages and sorts for stability.
-func (sc *symbolCollector) collectPackages(ctx context.Context, views []View) ([]*pkgView, error) {
-	var toWalk []*pkgView
-	for _, v := range views {
-		snapshot, release := v.Snapshot(ctx)
-		defer release()
-		knownPkgs, err := snapshot.KnownPackages(ctx)
-		if err != nil {
-			return nil, err
-		}
-		// TODO(rfindley): this can result in incomplete information in degraded
-		// memory mode.
-		workspacePackages, err := snapshot.ActivePackages(ctx)
-		if err != nil {
-			return nil, err
-		}
-		isWorkspacePkg := make(map[Package]bool)
-		for _, wp := range workspacePackages {
-			isWorkspacePkg[wp] = true
-		}
-		for _, pkg := range knownPkgs {
-			toWalk = append(toWalk, &pkgView{
-				pkg:         pkg,
-				isWorkspace: isWorkspacePkg[pkg],
-			})
-		}
-	}
-	// Now sort for stability of results. We order by
-	// (pkgView.isWorkspace, pkgView.p.ID())
-	sort.Slice(toWalk, func(i, j int) bool {
-		lhs := toWalk[i]
-		rhs := toWalk[j]
-		switch {
-		case lhs.isWorkspace == rhs.isWorkspace:
-			return lhs.pkg.ID() < rhs.pkg.ID()
-		case lhs.isWorkspace:
-			return true
-		default:
-			return false
-		}
-	})
-	return toWalk, nil
-}
-
-func (sc *symbolCollector) walkFilesDecls(decls []ast.Decl) {
-	for _, decl := range decls {
-		switch decl := decl.(type) {
-		case *ast.FuncDecl:
-			kind := protocol.Function
-			var recv *ast.Ident
-			if decl.Recv.NumFields() > 0 {
-				kind = protocol.Method
-				recv = unpackRecv(decl.Recv.List[0].Type)
-			}
-			if recv != nil {
-				sc.match(decl.Name.Name, kind, decl.Name, recv)
-			} else {
-				sc.match(decl.Name.Name, kind, decl.Name)
-			}
-		case *ast.GenDecl:
-			for _, spec := range decl.Specs {
-				switch spec := spec.(type) {
-				case *ast.TypeSpec:
-					sc.match(spec.Name.Name, typeToKind(sc.current.pkg.GetTypesInfo().TypeOf(spec.Type)), spec.Name)
-					sc.walkType(spec.Type, spec.Name)
-				case *ast.ValueSpec:
-					for _, name := range spec.Names {
-						kind := protocol.Variable
-						if decl.Tok == token.CONST {
-							kind = protocol.Constant
-						}
-						sc.match(name.Name, kind, name)
-					}
-				}
-			}
-		}
-	}
-}
-
-func unpackRecv(rtyp ast.Expr) *ast.Ident {
-	// Extract the receiver identifier. Lifted from go/types/resolver.go
-L:
-	for {
-		switch t := rtyp.(type) {
-		case *ast.ParenExpr:
-			rtyp = t.X
-		case *ast.StarExpr:
-			rtyp = t.X
-		default:
-			break L
-		}
-	}
-	if name, _ := rtyp.(*ast.Ident); name != nil {
-		return name
-	}
-	return nil
-}
-
-// walkType processes symbols related to a type expression. path is path of
-// nested type identifiers to the type expression.
-func (sc *symbolCollector) walkType(typ ast.Expr, path ...*ast.Ident) {
-	switch st := typ.(type) {
-	case *ast.StructType:
-		for _, field := range st.Fields.List {
-			sc.walkField(field, protocol.Field, protocol.Field, path...)
-		}
-	case *ast.InterfaceType:
-		for _, field := range st.Methods.List {
-			sc.walkField(field, protocol.Interface, protocol.Method, path...)
-		}
-	}
-}
-
-// walkField processes symbols related to the struct field or interface method.
-//
-// unnamedKind and namedKind are the symbol kinds if the field is resp. unnamed
-// or named. path is the path of nested identifiers containing the field.
-func (sc *symbolCollector) walkField(field *ast.Field, unnamedKind, namedKind protocol.SymbolKind, path ...*ast.Ident) {
-	if len(field.Names) == 0 {
-		switch typ := field.Type.(type) {
-		case *ast.SelectorExpr:
-			// embedded qualified type
-			sc.match(typ.Sel.Name, unnamedKind, field, path...)
-		default:
-			sc.match(types.ExprString(field.Type), unnamedKind, field, path...)
-		}
-	}
-	for _, name := range field.Names {
-		sc.match(name.Name, namedKind, name, path...)
-		sc.walkType(field.Type, append(path, name)...)
-	}
-}
-
 func typeToKind(typ types.Type) protocol.SymbolKind {
 	switch typ := typ.Underlying().(type) {
 	case *types.Interface:
@@ -475,126 +450,15 @@
 	return protocol.Variable
 }
 
-// match finds matches and gathers the symbol identified by name, kind and node
-// via the symbolCollector's matcher after first de-duping against previously
-// seen symbols.
-//
-// path specifies the identifier path to a nested field or interface method.
-func (sc *symbolCollector) match(name string, kind protocol.SymbolKind, node ast.Node, path ...*ast.Ident) {
-	if !node.Pos().IsValid() || !node.End().IsValid() {
-		return
-	}
-
-	isExported := isExported(name)
-	var names []string
-	for _, ident := range path {
-		names = append(names, ident.Name+".")
-		if !ident.IsExported() {
-			isExported = false
-		}
-	}
-	names = append(names, name)
-
-	// Factors to apply to the match score for the purpose of downranking
-	// results.
-	//
-	// These numbers were crudely calibrated based on trial-and-error using a
-	// small number of sample queries. Adjust as necessary.
-	//
-	// All factors are multiplicative, meaning if more than one applies they are
-	// multiplied together.
-	const (
-		// nonWorkspaceFactor is applied to symbols outside of any active
-		// workspace. Developers are less likely to want to jump to code that they
-		// are not actively working on.
-		nonWorkspaceFactor = 0.5
-		// nonWorkspaceUnexportedFactor is applied to unexported symbols outside of
-		// any active workspace. Since one wouldn't usually jump to unexported
-		// symbols to understand a package API, they are particularly irrelevant.
-		nonWorkspaceUnexportedFactor = 0.5
-		// fieldFactor is applied to fields and interface methods. One would
-		// typically jump to the type definition first, so ranking fields highly
-		// can be noisy.
-		fieldFactor = 0.5
-	)
-	symbol, score := sc.symbolizer(names, sc.current.pkg, sc.matcher)
-
-	// Downrank symbols outside of the workspace.
-	if !sc.current.isWorkspace {
-		score *= nonWorkspaceFactor
-		if !isExported {
-			score *= nonWorkspaceUnexportedFactor
-		}
-	}
-
-	// Downrank fields.
-	if len(path) > 0 {
-		score *= fieldFactor
-	}
-
-	// Avoid the work below if we know this score will not be sorted into the
-	// results.
-	if score <= sc.res[len(sc.res)-1].score {
-		return
-	}
-
-	rng, err := fileRange(sc.curFile, node.Pos(), node.End())
-	if err != nil {
-		return
-	}
-	si := symbolInformation{
-		score:     score,
-		name:      name,
-		symbol:    symbol,
-		container: sc.current.pkg.PkgPath(),
-		kind:      kind,
-		location: protocol.Location{
-			URI:   protocol.URIFromSpanURI(sc.curFile.URI),
-			Range: rng,
-		},
-	}
-	insertAt := sort.Search(len(sc.res), func(i int) bool {
-		return sc.res[i].score < score
-	})
-	if insertAt < len(sc.res)-1 {
-		copy(sc.res[insertAt+1:], sc.res[insertAt:len(sc.res)-1])
-	}
-	sc.res[insertAt] = si
-}
-
-func fileRange(pgf *ParsedGoFile, start, end token.Pos) (protocol.Range, error) {
-	s, err := span.FileSpan(pgf.Tok, pgf.Mapper.Converter, start, end)
-	if err != nil {
-		return protocol.Range{}, nil
-	}
-	return pgf.Mapper.Range(s)
-}
-
-// isExported reports if a token is exported. Copied from
-// token.IsExported (go1.13+).
-//
-// TODO: replace usage with token.IsExported once go1.12 is no longer
-// supported.
-func isExported(name string) bool {
-	ch, _ := utf8.DecodeRuneInString(name)
-	return unicode.IsUpper(ch)
-}
-
-// pkgView holds information related to a package that we are going to walk.
-type pkgView struct {
-	pkg         Package
-	isWorkspace bool
-}
-
 // symbolInformation is a cut-down version of protocol.SymbolInformation that
 // allows struct values of this type to be used as map keys.
 type symbolInformation struct {
 	score     float64
-	name      string
 	symbol    string
 	container string
 	kind      protocol.SymbolKind
-	location  protocol.Location
+	uri       span.URI
+	rng       protocol.Range
 }
 
 // asProtocolSymbolInformation converts s to a protocol.SymbolInformation value.
@@ -602,9 +466,12 @@
 // TODO: work out how to handle tags if/when they are needed.
 func (s symbolInformation) asProtocolSymbolInformation() protocol.SymbolInformation {
 	return protocol.SymbolInformation{
-		Name:          s.symbol,
-		Kind:          s.kind,
-		Location:      s.location,
+		Name: s.symbol,
+		Kind: s.kind,
+		Location: protocol.Location{
+			URI:   protocol.URIFromSpanURI(s.uri),
+			Range: s.rng,
+		},
 		ContainerName: s.container,
 	}
 }
diff --git a/internal/lsp/testdata/workspacesymbol/query.go.golden b/internal/lsp/testdata/workspacesymbol/query.go.golden
index 857ef3f..4c6d470 100644
--- a/internal/lsp/testdata/workspacesymbol/query.go.golden
+++ b/internal/lsp/testdata/workspacesymbol/query.go.golden
@@ -41,7 +41,7 @@
 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:14:6-12 main.myType Class
 workspacesymbol/main.go:18:18-26 main.myType.Blahblah Method
 
 -- workspace_symbol-casesensitive-main.myType.Blahblah --