internal/lsp/cache: minor simplifications to Symbols

Minor cleanups based on studying the code in preparation
for saving a persistent index:

- Remove unused error result from Symbols method.
- Remove unnecessary fields from symbolHandle.
- Add various explanatory comments.
- In workspace_symbols.go:
  - separate extract and match phases of collectSymbols clearly
  - replace symbolCollector and matchWorker types by simple parameters
  - combine loops (roots, buildMatcher)
  - move buildMatcher creation down to where it is needed.

Change-Id: Ifcad61a9a8c7d70f573024bcfa76d476552ee428
Reviewed-on: https://go-review.googlesource.com/c/tools/+/412822
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go
index 2a8a169..3640272 100644
--- a/internal/lsp/cache/cache.go
+++ b/internal/lsp/cache/cache.go
@@ -68,6 +68,7 @@
 	return true
 }
 
+// GetFile stats and (maybe) reads the file, updates the cache, and returns it.
 func (c *Cache) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
 	return c.getFile(ctx, uri)
 }
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 9875ae4..3268173 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -997,9 +997,7 @@
 }
 
 // Symbols extracts and returns the symbols for each file in all the snapshot's views.
-func (s *snapshot) Symbols(ctx context.Context) (map[span.URI][]source.Symbol, error) {
-	// Keep going on errors, but log the first failure.
-	// Partial results are better than no symbol results.
+func (s *snapshot) Symbols(ctx context.Context) map[span.URI][]source.Symbol {
 	var (
 		group    errgroup.Group
 		nprocs   = 2 * runtime.GOMAXPROCS(-1)  // symbolize is a mix of I/O and CPU
@@ -1023,10 +1021,12 @@
 			return nil
 		})
 	}
+	// Keep going on errors, but log the first failure.
+	// Partial results are better than no symbol results.
 	if err := group.Wait(); err != nil {
 		event.Error(ctx, "getting snapshot symbols", err)
 	}
-	return result, nil
+	return result
 }
 
 func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]source.Metadata, error) {
@@ -1137,11 +1137,10 @@
 	return s.symbols[uri]
 }
 
-func (s *snapshot) addSymbolHandle(sh *symbolHandle) *symbolHandle {
+func (s *snapshot) addSymbolHandle(uri span.URI, 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 {
@@ -1338,7 +1337,7 @@
 		return fh, nil
 	}
 
-	fh, err := s.view.session.cache.getFile(ctx, f.URI())
+	fh, err := s.view.session.cache.getFile(ctx, f.URI()) // read the file
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/cache/symbols.go b/internal/lsp/cache/symbols.go
index bf5e00b..d56a036 100644
--- a/internal/lsp/cache/symbols.go
+++ b/internal/lsp/cache/symbols.go
@@ -18,13 +18,9 @@
 	"golang.org/x/tools/internal/memoize"
 )
 
+// A symbolHandle contains a handle to the result of symbolizing a file.
 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.
@@ -33,30 +29,30 @@
 	err     error
 }
 
-type symbolHandleKey source.Hash
-
+// buildSymbolHandle returns a handle to the result of symbolizing a file,
+// if necessary creating it and saving it in the snapshot.
 func (s *snapshot) buildSymbolHandle(ctx context.Context, fh source.FileHandle) *symbolHandle {
 	if h := s.getSymbolHandle(fh.URI()); h != nil {
 		return h
 	}
+	type symbolHandleKey source.Hash
 	key := symbolHandleKey(fh.FileIdentity().Hash)
-	h := s.generation.Bind(key, func(_ context.Context, arg memoize.Arg) interface{} {
+	handle := s.generation.Bind(key, func(_ context.Context, arg memoize.Arg) interface{} {
 		snapshot := arg.(*snapshot)
-		data := &symbolData{}
-		data.symbols, data.err = symbolize(snapshot, fh)
-		return data
+		symbols, err := symbolize(snapshot, fh)
+		return &symbolData{symbols, err}
 	}, nil)
 
 	sh := &symbolHandle{
-		handle: h,
-		fh:     fh,
-		key:    key,
+		handle: handle,
 	}
-	return s.addSymbolHandle(sh)
+
+	return s.addSymbolHandle(fh.URI(), sh)
 }
 
-// symbolize extracts symbols from a file. It uses a parsed file already
-// present in the cache but otherwise does not populate the cache.
+// symbolize reads and parses a file and extracts symbols from it.
+// It may use a parsed file already present in the cache but
+// otherwise does not populate the cache.
 func symbolize(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, error) {
 	src, err := fh.Read()
 	if err != nil {
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 7960b0c..0d8d661 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -175,7 +175,7 @@
 	ActivePackages(ctx context.Context) ([]Package, error)
 
 	// Symbols returns all symbols in the snapshot.
-	Symbols(ctx context.Context) (map[span.URI][]Symbol, error)
+	Symbols(ctx context.Context) map[span.URI][]Symbol
 
 	// Metadata returns package metadata associated with the given file URI.
 	MetadataForFile(ctx context.Context, uri span.URI) ([]Metadata, error)
diff --git a/internal/lsp/source/workspace_symbol.go b/internal/lsp/source/workspace_symbol.go
index 11e22d1..c7cfe5c 100644
--- a/internal/lsp/source/workspace_symbol.go
+++ b/internal/lsp/source/workspace_symbol.go
@@ -50,14 +50,26 @@
 // with a different configured SymbolMatcher per View. Therefore we assume that
 // Session level configuration will define the SymbolMatcher to be used for the
 // WorkspaceSymbols method.
-func WorkspaceSymbols(ctx context.Context, matcherType SymbolMatcher, style SymbolStyle, views []View, query string) ([]protocol.SymbolInformation, error) {
+func WorkspaceSymbols(ctx context.Context, matcher SymbolMatcher, style SymbolStyle, views []View, query string) ([]protocol.SymbolInformation, error) {
 	ctx, done := event.Start(ctx, "source.WorkspaceSymbols")
 	defer done()
 	if query == "" {
 		return nil, nil
 	}
-	sc := newSymbolCollector(matcherType, style, query)
-	return sc.walk(ctx, views)
+
+	var s symbolizer
+	switch style {
+	case DynamicSymbols:
+		s = dynamicSymbolMatch
+	case FullyQualifiedSymbols:
+		s = fullyQualifiedSymbolMatch
+	case PackageQualifiedSymbols:
+		s = packageSymbolMatch
+	default:
+		panic(fmt.Errorf("unknown symbol style: %v", style))
+	}
+
+	return collectSymbols(ctx, views, matcher, s, query)
 }
 
 // A matcherFunc returns the index and score of a symbol match.
@@ -136,43 +148,6 @@
 	return nil, 0
 }
 
-// symbolCollector holds context as we walk Packages, gathering symbols that
-// match a given query.
-//
-// How we match symbols is parameterized by two interfaces:
-//   - A matcherFunc determines how well a string symbol matches a query. It
-//     returns a non-negative score indicating the quality of the match. A score
-//     of zero indicates no match.
-//   - A symbolizer determines how we extract the symbol for an object. This
-//     enables the 'symbolStyle' configuration option.
-type symbolCollector struct {
-	// These types parameterize the symbol-matching pass.
-	matchers   []matcherFunc
-	symbolizer symbolizer
-
-	symbolStore
-}
-
-func newSymbolCollector(matcher SymbolMatcher, style SymbolStyle, query string) *symbolCollector {
-	var s symbolizer
-	switch style {
-	case DynamicSymbols:
-		s = dynamicSymbolMatch
-	case FullyQualifiedSymbols:
-		s = fullyQualifiedSymbolMatch
-	case PackageQualifiedSymbols:
-		s = packageSymbolMatch
-	default:
-		panic(fmt.Errorf("unknown symbol style: %v", style))
-	}
-	sc := &symbolCollector{symbolizer: s}
-	sc.matchers = make([]matcherFunc, runtime.GOMAXPROCS(-1))
-	for i := range sc.matchers {
-		sc.matchers[i] = buildMatcher(matcher, query)
-	}
-	return sc
-}
-
 func buildMatcher(matcher SymbolMatcher, query string) matcherFunc {
 	switch matcher {
 	case SymbolFuzzy:
@@ -302,36 +277,42 @@
 	return first, score
 }
 
-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.
+// collectSymbols calls snapshot.Symbols to walk the syntax trees of
+// all files in the views' current snapshots, and returns a sorted,
+// scored list of symbols that best match the parameters.
+//
+// How it matches symbols is parameterized by two interfaces:
+//   - A matcherFunc determines how well a string symbol matches a query. It
+//     returns a non-negative score indicating the quality of the match. A score
+//     of zero indicates no match.
+//   - A symbolizer determines how we extract the symbol for an object. This
+//     enables the 'symbolStyle' configuration option.
+//
+func collectSymbols(ctx context.Context, views []View, matcherType SymbolMatcher, symbolizer symbolizer, query string) ([]protocol.SymbolInformation, error) {
+
+	// Extract symbols from all files.
+	var work []symbolFile
 	var roots []string
-	for _, v := range views {
-		roots = append(roots, strings.TrimRight(string(v.Folder()), "/"))
-	}
-
-	results := make(chan *symbolStore)
-	matcherlen := len(sc.matchers)
-	files := make(map[span.URI]symbolFile)
-
+	seen := make(map[span.URI]bool)
+	// TODO(adonovan): opt: parallelize this loop? How often is len > 1?
 	for _, v := range views {
 		snapshot, release := v.Snapshot(ctx)
 		defer release()
-		psyms, err := snapshot.Symbols(ctx)
-		if err != nil {
-			return nil, err
-		}
+
+		// Use the root view URIs for determining (lexically)
+		// whether a URI is in any open workspace.
+		roots = append(roots, strings.TrimRight(string(v.Folder()), "/"))
 
 		filters := v.Options().DirectoryFilters
 		folder := filepath.ToSlash(v.Folder().Filename())
-		for uri, syms := range psyms {
+		for uri, syms := range snapshot.Symbols(ctx) {
 			norm := filepath.ToSlash(uri.Filename())
 			nm := strings.TrimPrefix(norm, folder)
 			if FiltersDisallow(nm, filters) {
 				continue
 			}
 			// Only scan each file once.
-			if _, ok := files[uri]; ok {
+			if seen[uri] {
 				continue
 			}
 			mds, err := snapshot.MetadataForFile(ctx, uri)
@@ -343,39 +324,37 @@
 				// TODO: should use the bug reporting API
 				continue
 			}
-			files[uri] = symbolFile{uri, mds[0], syms}
+			seen[uri] = true
+			work = append(work, symbolFile{uri, mds[0], syms})
 		}
 	}
 
-	var work []symbolFile
-	for _, f := range files {
-		work = append(work, f)
-	}
-
-	// Compute matches concurrently. Each symbolWorker has its own symbolStore,
+	// Match symbols in parallel.
+	// Each worker has its own symbolStore,
 	// which we merge at the end.
-	for i, matcher := range sc.matchers {
-		go func(i int, matcher matcherFunc) {
-			w := &symbolWorker{
-				symbolizer: sc.symbolizer,
-				matcher:    matcher,
-				ss:         &symbolStore{},
-				roots:      roots,
+	nmatchers := runtime.GOMAXPROCS(-1) // matching is CPU bound
+	results := make(chan *symbolStore)
+	for i := 0; i < nmatchers; i++ {
+		go func(i int) {
+			matcher := buildMatcher(matcherType, query)
+			store := new(symbolStore)
+			// Assign files to workers in round-robin fashion.
+			for j := i; j < len(work); j += nmatchers {
+				matchFile(store, symbolizer, matcher, roots, work[j])
 			}
-			for j := i; j < len(work); j += matcherlen {
-				w.matchFile(work[j])
-			}
-			results <- w.ss
-		}(i, matcher)
+			results <- store
+		}(i)
 	}
 
-	for i := 0; i < matcherlen; i++ {
-		ss := <-results
-		for _, si := range ss.res {
-			sc.store(si)
+	// Gather and merge results as they arrive.
+	var unified symbolStore
+	for i := 0; i < nmatchers; i++ {
+		store := <-results
+		for _, syms := range store.res {
+			unified.store(syms)
 		}
 	}
-	return sc.results(), nil
+	return unified.results(), nil
 }
 
 // FilterDisallow is code from the body of cache.pathExcludedByFilter in cache/view.go
@@ -407,20 +386,13 @@
 	syms []Symbol
 }
 
-// symbolWorker matches symbols and captures the highest scoring results.
-type symbolWorker struct {
-	symbolizer symbolizer
-	matcher    matcherFunc
-	ss         *symbolStore
-	roots      []string
-}
-
-func (w *symbolWorker) matchFile(i symbolFile) {
+// matchFile scans a symbol file and adds matching symbols to the store.
+func matchFile(store *symbolStore, symbolizer symbolizer, matcher matcherFunc, roots []string, i symbolFile) {
 	for _, sym := range i.syms {
-		symbolParts, score := w.symbolizer(sym.Name, i.md, w.matcher)
+		symbolParts, score := symbolizer(sym.Name, i.md, matcher)
 
 		// Check if the score is too low before applying any downranking.
-		if w.ss.tooLow(score) {
+		if store.tooLow(score) {
 			continue
 		}
 
@@ -463,7 +435,7 @@
 		}
 
 		inWorkspace := false
-		for _, root := range w.roots {
+		for _, root := range roots {
 			if strings.HasPrefix(string(i.uri), root) {
 				inWorkspace = true
 				break
@@ -484,7 +456,7 @@
 		}
 		score *= 1.0 - depth*depthFactor
 
-		if w.ss.tooLow(score) {
+		if store.tooLow(score) {
 			continue
 		}
 
@@ -496,7 +468,7 @@
 			rng:       sym.Range,
 			container: i.md.PackagePath(),
 		}
-		w.ss.store(si)
+		store.store(si)
 	}
 }