all: merge master into gopls-release-branch.0.4

Figured it was easier to just merge to master instead of cherry-picking.

Change-Id: Ia78e95bd1dc4e6b07bd5f9be73118f99f9827091
diff --git a/godoc/vfs/mapfs/mapfs.go b/godoc/vfs/mapfs/mapfs.go
index 5de7ce7..9d0f465 100644
--- a/godoc/vfs/mapfs/mapfs.go
+++ b/godoc/vfs/mapfs/mapfs.go
@@ -7,6 +7,7 @@
 package mapfs // import "golang.org/x/tools/godoc/vfs/mapfs"
 
 import (
+	"fmt"
 	"io"
 	"os"
 	pathpkg "path"
@@ -18,9 +19,21 @@
 )
 
 // New returns a new FileSystem from the provided map.
-// Map keys should be forward slash-separated pathnames
-// and not contain a leading slash.
+// Map keys must be forward slash-separated paths with
+// no leading slash, such as "file1.txt" or "dir/file2.txt".
+// New panics if any of the paths contain a leading slash.
 func New(m map[string]string) vfs.FileSystem {
+	// Verify all provided paths are relative before proceeding.
+	var pathsWithLeadingSlash []string
+	for p := range m {
+		if strings.HasPrefix(p, "/") {
+			pathsWithLeadingSlash = append(pathsWithLeadingSlash, p)
+		}
+	}
+	if len(pathsWithLeadingSlash) > 0 {
+		panic(fmt.Errorf("mapfs.New: invalid paths with a leading slash: %q", pathsWithLeadingSlash))
+	}
+
 	return mapFS(m)
 }
 
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md
index 8c3d62a..7f0c22e 100644
--- a/gopls/doc/settings.md
+++ b/gopls/doc/settings.md
@@ -83,9 +83,15 @@
 ...
 ```
 
-### **staticcheck** *boolean*
+### **codelens** *map[string]bool*
 
-If true, it enables the use of the staticcheck.io analyzers.
+Overrides the enabled/disabled state of various code lenses. Currently, we
+support two code lenses:
+
+* `generate`: run `go generate` as specified by a `//go:generate` directive.
+* `upgrade.dependency`: upgrade a dependency listed in a `go.mod` file.
+
+By default, both of these code lenses are enabled.
 
 ### **completionDocumentation** *boolean*
 
@@ -129,3 +135,27 @@
 If true, this enables server side fuzzy matching of completion candidates.
 
 Default: `true`.
+
+### **staticcheck** *boolean*
+
+If true, it enables the use of the staticcheck.io analyzers.
+
+### **matcher** *string*
+
+Defines the algorithm that is used when calculating completion candidates. Must be one of:
+
+* `"fuzzy"`
+* `"caseSensitive"`
+* `"caseInsensitive"`
+
+Default: `"caseInsensitive"`.
+
+### **symbolMatcher** *string*
+
+Defines the algorithm that is used when calculating workspace symbol results. Must be one of:
+
+* `"fuzzy"`
+* `"caseSensitive"`
+* `"caseInsensitive"`
+
+Default: `"caseInsensitive"`.
diff --git a/gopls/doc/vscode.md b/gopls/doc/vscode.md
index f0aff81..60c196e 100644
--- a/gopls/doc/vscode.md
+++ b/gopls/doc/vscode.md
@@ -52,6 +52,16 @@
 
 You can disable features through the `"go.languageServerExperimentalFeatures"` section of the config. An example of a feature you may want to disable is `"documentLink"`, which opens [`pkg.go.dev`](https://pkg.go.dev) links when you click on import statements in your file.
 
+### Build tags
+
+build tags will not be picked from `go.buildTags` configuration section, instead they should be specified as part of the`GOFLAGS` environment variable:
+
+```json5
+"go.toolsEnvVars": {
+    "GOFLAGS": "-tags=<yourtag>"
+}
+```
+
 
 [VSCode-Go]: https://github.com/microsoft/vscode-go
 
diff --git a/internal/event/export/log.go b/internal/event/export/log.go
index d36bb0c..96110e7 100644
--- a/internal/event/export/log.go
+++ b/internal/event/export/log.go
@@ -12,7 +12,6 @@
 
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/event/core"
-	"golang.org/x/tools/internal/event/keys"
 	"golang.org/x/tools/internal/event/label"
 )
 
@@ -27,7 +26,7 @@
 
 type logWriter struct {
 	mu         sync.Mutex
-	buffer     [128]byte
+	printer    Printer
 	writer     io.Writer
 	onlyErrors bool
 }
@@ -40,28 +39,7 @@
 		}
 		w.mu.Lock()
 		defer w.mu.Unlock()
-
-		buf := w.buffer[:0]
-		if !ev.At().IsZero() {
-			w.writer.Write(ev.At().AppendFormat(buf, "2006/01/02 15:04:05 "))
-		}
-		msg := keys.Msg.Get(lm)
-		io.WriteString(w.writer, msg)
-		if err := keys.Err.Get(lm); err != nil {
-			io.WriteString(w.writer, ": ")
-			io.WriteString(w.writer, err.Error())
-		}
-		for index := 0; ev.Valid(index); index++ {
-			l := ev.Label(index)
-			if !l.Valid() || l.Key() == keys.Msg || l.Key() == keys.Err {
-				continue
-			}
-			io.WriteString(w.writer, "\n\t")
-			io.WriteString(w.writer, l.Key().Name())
-			io.WriteString(w.writer, "=")
-			l.Key().Format(w.writer, buf, l)
-		}
-		io.WriteString(w.writer, "\n")
+		w.printer.WriteEvent(w.writer, ev, lm)
 
 	case event.IsStart(ev):
 		if span := GetSpan(ctx); span != nil {
diff --git a/internal/event/export/printer.go b/internal/event/export/printer.go
new file mode 100644
index 0000000..9fb6f9e
--- /dev/null
+++ b/internal/event/export/printer.go
@@ -0,0 +1,43 @@
+// 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 export
+
+import (
+	"io"
+
+	"golang.org/x/tools/internal/event/core"
+	"golang.org/x/tools/internal/event/keys"
+	"golang.org/x/tools/internal/event/label"
+)
+
+type Printer struct {
+	buffer [128]byte
+}
+
+func (p *Printer) WriteEvent(w io.Writer, ev core.Event, lm label.Map) {
+	buf := p.buffer[:0]
+	if !ev.At().IsZero() {
+		w.Write(ev.At().AppendFormat(buf, "2006/01/02 15:04:05 "))
+	}
+	msg := keys.Msg.Get(lm)
+	io.WriteString(w, msg)
+	if err := keys.Err.Get(lm); err != nil {
+		if msg != "" {
+			io.WriteString(w, ": ")
+		}
+		io.WriteString(w, err.Error())
+	}
+	for index := 0; ev.Valid(index); index++ {
+		l := ev.Label(index)
+		if !l.Valid() || l.Key() == keys.Msg || l.Key() == keys.Err {
+			continue
+		}
+		io.WriteString(w, "\n\t")
+		io.WriteString(w, l.Key().Name())
+		io.WriteString(w, "=")
+		l.Key().Format(w, buf, l)
+	}
+	io.WriteString(w, "\n")
+}
diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go
index 14d2bf6..35336a7 100644
--- a/internal/lsp/cache/analysis.go
+++ b/internal/lsp/cache/analysis.go
@@ -147,7 +147,7 @@
 	})
 	act.handle = h
 
-	s.addActionHandle(act)
+	act = s.addActionHandle(act)
 	return act, nil
 }
 
diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go
index 4cf17e3..6d16b31 100644
--- a/internal/lsp/cache/check.go
+++ b/internal/lsp/cache/check.go
@@ -97,8 +97,11 @@
 	})
 	ph.handle = h
 
-	// Cache the PackageHandle in the snapshot.
-	s.addPackage(ph)
+	// Cache the PackageHandle in the snapshot. If a package handle has already
+	// been cached, addPackage will return the cached value. This is fine,
+	// since the original package handle above will have no references and be
+	// garbage collected.
+	ph = s.addPackageHandle(ph)
 
 	return ph, nil
 }
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 4f3bffb..3da975d 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -419,6 +419,7 @@
 	return overlays, nil
 }
 
+// GetFile implements the source.FileSystem interface.
 func (s *Session) GetFile(uri span.URI) source.FileHandle {
 	if overlay := s.readOverlay(uri); overlay != nil {
 		return overlay
@@ -436,3 +437,16 @@
 	}
 	return nil
 }
+
+func (s *Session) UnsavedFiles() []span.URI {
+	s.overlayMu.Lock()
+	defer s.overlayMu.Unlock()
+
+	var unsaved []span.URI
+	for uri, overlay := range s.overlays {
+		if !overlay.saved {
+			unsaved = append(unsaved, uri)
+		}
+	}
+	return unsaved
+}
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 4e0e7c5..df90d18 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -291,17 +291,17 @@
 	}
 }
 
-func (s *snapshot) addPackage(ph *packageHandle) {
+func (s *snapshot) addPackageHandle(ph *packageHandle) *packageHandle {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
-	// TODO: We should make sure not to compute duplicate packageHandles,
-	// and instead panic here. This will be hard to do because we may encounter
-	// the same package multiple times in the dependency tree.
-	if _, ok := s.packages[ph.packageKey()]; ok {
-		return
+	// If the package handle has already been cached,
+	// return the cached handle instead of overriding it.
+	if ph, ok := s.packages[ph.packageKey()]; ok {
+		return ph
 	}
 	s.packages[ph.packageKey()] = ph
+	return ph
 }
 
 func (s *snapshot) workspacePackageIDs() (ids []packageID) {
@@ -427,7 +427,7 @@
 	return s.actions[key]
 }
 
-func (s *snapshot) addActionHandle(ah *actionHandle) {
+func (s *snapshot) addActionHandle(ah *actionHandle) *actionHandle {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
@@ -438,10 +438,11 @@
 			mode: ah.pkg.mode,
 		},
 	}
-	if _, ok := s.actions[key]; ok {
-		return
+	if ah, ok := s.actions[key]; ok {
+		return ah
 	}
 	s.actions[key] = ah
+	return ah
 }
 
 func (s *snapshot) getIDsForURI(uri span.URI) []packageID {
diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go
index 5d4c0e3..64011e1 100644
--- a/internal/lsp/cmd/cmd.go
+++ b/internal/lsp/cmd/cmd.go
@@ -241,10 +241,10 @@
 	return connection, connection.initialize(ctx, app.options)
 }
 
-var matcherString = map[source.Matcher]string{
-	source.Fuzzy:           "fuzzy",
-	source.CaseSensitive:   "caseSensitive",
-	source.CaseInsensitive: "default",
+var matcherString = map[source.SymbolMatcher]string{
+	source.SymbolFuzzy:           "fuzzy",
+	source.SymbolCaseSensitive:   "caseSensitive",
+	source.SymbolCaseInsensitive: "default",
 }
 
 func (c *connection) initialize(ctx context.Context, options func(*source.Options)) error {
@@ -262,7 +262,7 @@
 	}
 	params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = opts.HierarchicalDocumentSymbolSupport
 	params.InitializationOptions = map[string]interface{}{
-		"matcher": matcherString[opts.Matcher],
+		"symbolMatcher": matcherString[opts.SymbolMatcher],
 	}
 	if _, err := c.Server.Initialize(ctx, params); err != nil {
 		return err
diff --git a/internal/lsp/cmd/inspect.go b/internal/lsp/cmd/inspect.go
index 4834c70..d3f08b7 100644
--- a/internal/lsp/cmd/inspect.go
+++ b/internal/lsp/cmd/inspect.go
@@ -6,8 +6,11 @@
 
 import (
 	"context"
+	"encoding/json"
 	"flag"
 	"fmt"
+	"log"
+	"os"
 
 	"golang.org/x/tools/internal/lsp/lsprpc"
 	"golang.org/x/tools/internal/tool"
@@ -89,18 +92,10 @@
 	if err != nil {
 		return err
 	}
-	fmt.Printf("Server logfile: %s\n", state.Logfile)
-	fmt.Printf("Server debug address: %v\n", state.DebugAddr)
-	for _, c := range state.Clients {
-		if c.ClientID == state.CurrentClientID {
-			// This is the client for the listsessions command itself.
-			continue
-		}
-		fmt.Println()
-		fmt.Printf("Client %s:\n", c.ClientID)
-		fmt.Printf("\tsession: %s:\n", c.SessionID)
-		fmt.Printf("\tlogfile: %s:\n", c.Logfile)
-		fmt.Printf("\tdebug address: %s:\n", c.DebugAddr)
+	v, err := json.MarshalIndent(state, "", "\t")
+	if err != nil {
+		log.Fatal(err)
 	}
+	os.Stdout.Write(v)
 	return nil
 }
diff --git a/internal/lsp/cmd/workspace_symbol.go b/internal/lsp/cmd/workspace_symbol.go
index f0b2302..b263262 100644
--- a/internal/lsp/cmd/workspace_symbol.go
+++ b/internal/lsp/cmd/workspace_symbol.go
@@ -47,11 +47,11 @@
 		}
 		switch r.Matcher {
 		case "fuzzy":
-			o.Matcher = source.Fuzzy
+			o.SymbolMatcher = source.SymbolFuzzy
 		case "caseSensitive":
-			o.Matcher = source.CaseSensitive
+			o.SymbolMatcher = source.SymbolCaseSensitive
 		default:
-			o.Matcher = source.CaseInsensitive
+			o.SymbolMatcher = source.SymbolCaseInsensitive
 		}
 	}
 
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index 280f47e..cd80d6d 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -18,13 +18,13 @@
 
 func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
 	switch params.Command {
-	case "generate":
+	case source.CommandGenerate:
 		dir, recursive, err := getGenerateRequest(params.Arguments)
 		if err != nil {
 			return nil, err
 		}
 		go s.runGenerate(xcontext.Detach(ctx), dir, recursive)
-	case "tidy":
+	case source.CommandTidy:
 		if len(params.Arguments) == 0 || len(params.Arguments) > 1 {
 			return nil, errors.Errorf("expected one file URI for call to `go mod tidy`, got %v", params.Arguments)
 		}
@@ -45,7 +45,7 @@
 		if _, err := gocmdRunner.Run(ctx, inv); err != nil {
 			return nil, err
 		}
-	case "upgrade.dependency":
+	case source.CommandUpgradeDependency:
 		if len(params.Arguments) < 2 {
 			return nil, errors.Errorf("expected one file URI and one dependency for call to `go get`, got %v", params.Arguments)
 		}
diff --git a/internal/lsp/debug/serve.go b/internal/lsp/debug/serve.go
index 64b6447..15c0d30 100644
--- a/internal/lsp/debug/serve.go
+++ b/internal/lsp/debug/serve.go
@@ -545,6 +545,8 @@
 }
 
 func makeGlobalExporter(stderr io.Writer) event.Exporter {
+	p := export.Printer{}
+	var pMu sync.Mutex
 	return func(ctx context.Context, ev core.Event, lm label.Map) context.Context {
 		i := GetInstance(ctx)
 
@@ -555,7 +557,9 @@
 			}
 			// Make sure any log messages without an instance go to stderr.
 			if i == nil {
-				fmt.Fprintf(stderr, "%v\n", ev)
+				pMu.Lock()
+				p.WriteEvent(stderr, ev, lm)
+				pMu.Unlock()
 			}
 		}
 		ctx = protocol.LogEvent(ctx, ev, lm)
diff --git a/internal/lsp/diff/difftest/difftest_test.go b/internal/lsp/diff/difftest/difftest_test.go
index 21d9cd1..f873543 100644
--- a/internal/lsp/diff/difftest/difftest_test.go
+++ b/internal/lsp/diff/difftest/difftest_test.go
@@ -2,10 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// As of writing illumos uses a version of diff for which `diff -u` reports
-// locations differently from GNU diff.
-// +build !illumos
-
 // Package difftest supplies a set of tests that will operate on any
 // implementation of a diff algorithm as exposed by
 // "golang.org/x/tools/internal/lsp/diff"
diff --git a/internal/lsp/fake/client.go b/internal/lsp/fake/client.go
index 4ee164c..9a2d8c9 100644
--- a/internal/lsp/fake/client.go
+++ b/internal/lsp/fake/client.go
@@ -10,59 +10,25 @@
 	"golang.org/x/tools/internal/lsp/protocol"
 )
 
+// ClientHooks are called to handle the corresponding client LSP method.
+type ClientHooks struct {
+	OnLogMessage             func(context.Context, *protocol.LogMessageParams) error
+	OnDiagnostics            func(context.Context, *protocol.PublishDiagnosticsParams) error
+	OnWorkDoneProgressCreate func(context.Context, *protocol.WorkDoneProgressCreateParams) error
+	OnProgress               func(context.Context, *protocol.ProgressParams) error
+	OnShowMessage            func(context.Context, *protocol.ShowMessageParams) error
+}
+
 // Client is an adapter that converts an *Editor into an LSP Client. It mosly
 // delegates functionality to hooks that can be configured by tests.
 type Client struct {
-	*Editor
-
-	// Hooks for testing. Add additional hooks here as needed for testing.
-	onLogMessage             func(context.Context, *protocol.LogMessageParams) error
-	onDiagnostics            func(context.Context, *protocol.PublishDiagnosticsParams) error
-	onWorkDoneProgressCreate func(context.Context, *protocol.WorkDoneProgressCreateParams) error
-	onProgress               func(context.Context, *protocol.ProgressParams) error
-	onShowMessage            func(context.Context, *protocol.ShowMessageParams) error
-}
-
-// OnShowMessage sets the hook to run when the editor receives a showMessage notification
-func (c *Client) OnShowMessage(hook func(context.Context, *protocol.ShowMessageParams) error) {
-	c.mu.Lock()
-	c.onShowMessage = hook
-	c.mu.Unlock()
-}
-
-// OnLogMessage sets the hook to run when the editor receives a log message.
-func (c *Client) OnLogMessage(hook func(context.Context, *protocol.LogMessageParams) error) {
-	c.mu.Lock()
-	c.onLogMessage = hook
-	c.mu.Unlock()
-}
-
-// OnDiagnostics sets the hook to run when the editor receives diagnostics
-// published from the language server.
-func (c *Client) OnDiagnostics(hook func(context.Context, *protocol.PublishDiagnosticsParams) error) {
-	c.mu.Lock()
-	c.onDiagnostics = hook
-	c.mu.Unlock()
-}
-
-func (c *Client) OnWorkDoneProgressCreate(hook func(context.Context, *protocol.WorkDoneProgressCreateParams) error) {
-	c.mu.Lock()
-	c.onWorkDoneProgressCreate = hook
-	c.mu.Unlock()
-}
-
-func (c *Client) OnProgress(hook func(context.Context, *protocol.ProgressParams) error) {
-	c.mu.Lock()
-	c.onProgress = hook
-	c.mu.Unlock()
+	editor *Editor
+	hooks  ClientHooks
 }
 
 func (c *Client) ShowMessage(ctx context.Context, params *protocol.ShowMessageParams) error {
-	c.mu.Lock()
-	c.lastMessage = params
-	c.mu.Unlock()
-	if c.onShowMessage != nil {
-		return c.onShowMessage(ctx, params)
+	if c.hooks.OnShowMessage != nil {
+		return c.hooks.OnShowMessage(ctx, params)
 	}
 	return nil
 }
@@ -72,30 +38,19 @@
 }
 
 func (c *Client) LogMessage(ctx context.Context, params *protocol.LogMessageParams) error {
-	c.mu.Lock()
-	c.logs = append(c.logs, params)
-	onLogMessage := c.onLogMessage
-	c.mu.Unlock()
-	if onLogMessage != nil {
-		return onLogMessage(ctx, params)
+	if c.hooks.OnLogMessage != nil {
+		return c.hooks.OnLogMessage(ctx, params)
 	}
 	return nil
 }
 
 func (c *Client) Event(ctx context.Context, event *interface{}) error {
-	c.mu.Lock()
-	c.events = append(c.events, event)
-	c.mu.Unlock()
 	return nil
 }
 
 func (c *Client) PublishDiagnostics(ctx context.Context, params *protocol.PublishDiagnosticsParams) error {
-	c.mu.Lock()
-	c.diagnostics = params
-	onPublishDiagnostics := c.onDiagnostics
-	c.mu.Unlock()
-	if onPublishDiagnostics != nil {
-		return onPublishDiagnostics(ctx, params)
+	if c.hooks.OnDiagnostics != nil {
+		return c.hooks.OnDiagnostics(ctx, params)
 	}
 	return nil
 }
@@ -110,7 +65,7 @@
 		if item.Section != "gopls" {
 			continue
 		}
-		results[i] = c.configuration()
+		results[i] = c.editor.configuration()
 	}
 	return results, nil
 }
@@ -124,21 +79,15 @@
 }
 
 func (c *Client) Progress(ctx context.Context, params *protocol.ProgressParams) error {
-	c.mu.Lock()
-	onProgress := c.onProgress
-	c.mu.Unlock()
-	if onProgress != nil {
-		return onProgress(ctx, params)
+	if c.hooks.OnProgress != nil {
+		return c.hooks.OnProgress(ctx, params)
 	}
 	return nil
 }
 
 func (c *Client) WorkDoneProgressCreate(ctx context.Context, params *protocol.WorkDoneProgressCreateParams) error {
-	c.mu.Lock()
-	onCreate := c.onWorkDoneProgressCreate
-	c.mu.Unlock()
-	if onCreate != nil {
-		return onCreate(ctx, params)
+	if c.hooks.OnWorkDoneProgressCreate != nil {
+		return c.hooks.OnWorkDoneProgressCreate(ctx, params)
 	}
 	return nil
 }
@@ -150,9 +99,9 @@
 		return &protocol.ApplyWorkspaceEditResponse{FailureReason: "Edit.Changes is unsupported"}, nil
 	}
 	for _, change := range params.Edit.DocumentChanges {
-		path := c.sandbox.Workdir.URIToPath(change.TextDocument.URI)
+		path := c.editor.sandbox.Workdir.URIToPath(change.TextDocument.URI)
 		edits := convertEdits(change.Edits)
-		c.EditBuffer(ctx, path, edits)
+		c.editor.EditBuffer(ctx, path, edits)
 	}
 	return &protocol.ApplyWorkspaceEditResponse{Applied: true}, nil
 }
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
index 9116d27..9ae5b84 100644
--- a/internal/lsp/fake/editor.go
+++ b/internal/lsp/fake/editor.go
@@ -20,8 +20,10 @@
 // Editor is a fake editor client.  It keeps track of client state and can be
 // used for writing LSP tests.
 type Editor struct {
-	// server, client, and sandbox are concurrency safe and written only at
-	// construction, so do not require synchronization.
+	Config EditorConfig
+
+	// server, client, and sandbox are concurrency safe and written only
+	// at construction time, so do not require synchronization.
 	server  protocol.Server
 	client  *Client
 	sandbox *Sandbox
@@ -30,11 +32,7 @@
 	// locking.
 	mu sync.Mutex
 	// Editor state.
-	buffers     map[string]buffer
-	lastMessage *protocol.ShowMessageParams
-	logs        []*protocol.LogMessageParams
-	diagnostics *protocol.PublishDiagnosticsParams
-	events      []interface{}
+	buffers map[string]buffer
 	// Capabilities / Options
 	serverCapabilities protocol.ServerCapabilities
 }
@@ -49,11 +47,30 @@
 	return strings.Join(b.content, "\n")
 }
 
+// EditorConfig configures the editor's LSP session. This is similar to
+// source.UserOptions, but we use a separate type here so that we expose only
+// that configuration which we support.
+//
+// The zero value for EditorConfig should correspond to its defaults.
+type EditorConfig struct {
+	Env []string
+
+	// CodeLens is a map defining whether codelens are enabled, keyed by the
+	// codeLens command. CodeLens which are not present in this map are left in
+	// their default state.
+	CodeLens map[string]bool
+
+	// SymbolMatcher is the config associated with the "symbolMatcher" gopls
+	// config option.
+	SymbolMatcher *string
+}
+
 // NewEditor Creates a new Editor.
-func NewEditor(ws *Sandbox) *Editor {
+func NewEditor(ws *Sandbox, config EditorConfig) *Editor {
 	return &Editor{
 		buffers: make(map[string]buffer),
 		sandbox: ws,
+		Config:  config,
 	}
 }
 
@@ -63,9 +80,9 @@
 //
 // It returns the editor, so that it may be called as follows:
 //   editor, err := NewEditor(s).Connect(ctx, conn)
-func (e *Editor) Connect(ctx context.Context, conn *jsonrpc2.Conn) (*Editor, error) {
+func (e *Editor) Connect(ctx context.Context, conn *jsonrpc2.Conn, hooks ClientHooks) (*Editor, error) {
 	e.server = protocol.ServerDispatcher(conn)
-	e.client = &Client{Editor: e}
+	e.client = &Client{editor: e, hooks: hooks}
 	go conn.Run(ctx,
 		protocol.Handlers(
 			protocol.ClientHandler(e.client,
@@ -105,15 +122,28 @@
 }
 
 func (e *Editor) configuration() map[string]interface{} {
+	config := map[string]interface{}{
+		"verboseWorkDoneProgress": true,
+	}
+
+	envvars := e.sandbox.GoEnv()
+	envvars = append(envvars, e.Config.Env...)
 	env := map[string]interface{}{}
-	for _, value := range e.sandbox.GoEnv() {
+	for _, value := range envvars {
 		kv := strings.SplitN(value, "=", 2)
 		env[kv[0]] = kv[1]
 	}
-	return map[string]interface{}{
-		"env":                     env,
-		"verboseWorkDoneProgress": true,
+	config["env"] = env
+
+	if e.Config.CodeLens != nil {
+		config["codelens"] = e.Config.CodeLens
 	}
+
+	if e.Config.SymbolMatcher != nil {
+		config["symbolMatcher"] = *e.Config.SymbolMatcher
+	}
+
+	return config
 }
 
 func (e *Editor) initialize(ctx context.Context) error {
@@ -235,9 +265,7 @@
 
 	if e.server != nil {
 		if err := e.server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{
-			TextDocument: protocol.TextDocumentIdentifier{
-				URI: e.sandbox.Workdir.URI(path),
-			},
+			TextDocument: e.textDocumentIdentifier(path),
 		}); err != nil {
 			return fmt.Errorf("DidClose: %w", err)
 		}
@@ -245,6 +273,12 @@
 	return nil
 }
 
+func (e *Editor) textDocumentIdentifier(path string) protocol.TextDocumentIdentifier {
+	return protocol.TextDocumentIdentifier{
+		URI: e.sandbox.Workdir.URI(path),
+	}
+}
+
 // SaveBuffer writes the content of the buffer specified by the given path to
 // the filesystem.
 func (e *Editor) SaveBuffer(ctx context.Context, path string) error {
@@ -269,9 +303,7 @@
 	}
 	e.mu.Unlock()
 
-	docID := protocol.TextDocumentIdentifier{
-		URI: e.sandbox.Workdir.URI(buf.path),
-	}
+	docID := e.textDocumentIdentifier(buf.path)
 	if e.server != nil {
 		if err := e.server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{
 			TextDocument: docID,
@@ -456,10 +488,8 @@
 	}
 	params := &protocol.DidChangeTextDocumentParams{
 		TextDocument: protocol.VersionedTextDocumentIdentifier{
-			Version: float64(buf.version),
-			TextDocumentIdentifier: protocol.TextDocumentIdentifier{
-				URI: e.sandbox.Workdir.URI(buf.path),
-			},
+			Version:                float64(buf.version),
+			TextDocumentIdentifier: e.textDocumentIdentifier(buf.path),
 		},
 		ContentChanges: evts,
 	}
@@ -614,3 +644,24 @@
 	// the caller.
 	return nil
 }
+
+// CodeLens execute a codelens request on the server.
+func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens, error) {
+	if e.server == nil {
+		return nil, nil
+	}
+	e.mu.Lock()
+	_, ok := e.buffers[path]
+	e.mu.Unlock()
+	if !ok {
+		return nil, fmt.Errorf("buffer %q is not open", path)
+	}
+	params := &protocol.CodeLensParams{
+		TextDocument: e.textDocumentIdentifier(path),
+	}
+	lens, err := e.server.CodeLens(ctx, params)
+	if err != nil {
+		return nil, err
+	}
+	return lens, nil
+}
diff --git a/internal/lsp/fake/editor_test.go b/internal/lsp/fake/editor_test.go
index e45984b..b34be17 100644
--- a/internal/lsp/fake/editor_test.go
+++ b/internal/lsp/fake/editor_test.go
@@ -54,7 +54,7 @@
 	}
 	defer ws.Close()
 	ctx := context.Background()
-	editor := NewEditor(ws)
+	editor := NewEditor(ws, EditorConfig{})
 	if err := editor.OpenFile(ctx, "main.go"); err != nil {
 		t.Fatal(err)
 	}
diff --git a/internal/lsp/fake/sandbox.go b/internal/lsp/fake/sandbox.go
index d097600..cee2993 100644
--- a/internal/lsp/fake/sandbox.go
+++ b/internal/lsp/fake/sandbox.go
@@ -22,7 +22,6 @@
 	name    string
 	gopath  string
 	basedir string
-	env     []string
 	Proxy   *Proxy
 	Workdir *Workdir
 }
@@ -31,10 +30,9 @@
 // working directory populated by the txtar-encoded content in srctxt, and a
 // file-based module proxy populated with the txtar-encoded content in
 // proxytxt.
-func NewSandbox(name, srctxt, proxytxt string, inGopath bool, env ...string) (_ *Sandbox, err error) {
+func NewSandbox(name, srctxt, proxytxt string, inGopath bool) (_ *Sandbox, err error) {
 	sb := &Sandbox{
 		name: name,
-		env:  env,
 	}
 	defer func() {
 		// Clean up if we fail at any point in this constructor.
@@ -103,12 +101,12 @@
 // GoEnv returns the default environment variables that can be used for
 // invoking Go commands in the sandbox.
 func (sb *Sandbox) GoEnv() []string {
-	return append([]string{
+	return []string{
 		"GOPATH=" + sb.GOPATH(),
 		"GOPROXY=" + sb.Proxy.GOPROXY(),
 		"GO111MODULE=",
 		"GOSUMDB=off",
-	}, sb.env...)
+	}
 }
 
 // RunGoCommand executes a go command in the sandbox.
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 04cae0d..ff42115 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -820,71 +820,41 @@
 }
 
 func (r *runner) WorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
-	got := r.callWorkspaceSymbols(t, query, func(opts *source.Options) {
-		opts.Matcher = source.CaseInsensitive
-	})
-	got = tests.FilterWorkspaceSymbols(got, dirs)
-	if len(got) != len(expectedSymbols) {
-		t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
-		return
-	}
-	if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
-		t.Error(diff)
-	}
+	r.callWorkspaceSymbols(t, query, source.SymbolCaseInsensitive, dirs, expectedSymbols)
 }
 
 func (r *runner) FuzzyWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
-	got := r.callWorkspaceSymbols(t, query, func(opts *source.Options) {
-		opts.Matcher = source.Fuzzy
-	})
-	got = tests.FilterWorkspaceSymbols(got, dirs)
-	if len(got) != len(expectedSymbols) {
-		t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
-		return
-	}
-	if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
-		t.Error(diff)
-	}
+	r.callWorkspaceSymbols(t, query, source.SymbolFuzzy, dirs, expectedSymbols)
 }
 
 func (r *runner) CaseSensitiveWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
-	got := r.callWorkspaceSymbols(t, query, func(opts *source.Options) {
-		opts.Matcher = source.CaseSensitive
-	})
-	got = tests.FilterWorkspaceSymbols(got, dirs)
-	if len(got) != len(expectedSymbols) {
-		t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
-		return
-	}
-	if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
-		t.Error(diff)
-	}
+	r.callWorkspaceSymbols(t, query, source.SymbolCaseSensitive, dirs, expectedSymbols)
 }
 
-func (r *runner) callWorkspaceSymbols(t *testing.T, query string, options func(*source.Options)) []protocol.SymbolInformation {
+func (r *runner) callWorkspaceSymbols(t *testing.T, query string, matcher source.SymbolMatcher, dirs map[string]struct{}, expectedSymbols []protocol.SymbolInformation) {
 	t.Helper()
 
-	for _, view := range r.server.session.Views() {
-		original := view.Options()
-		modified := original
-		options(&modified)
-		var err error
-		view, err = view.SetOptions(r.ctx, modified)
-		if err != nil {
-			t.Error(err)
-			return nil
-		}
-		defer view.SetOptions(r.ctx, original)
-	}
+	original := r.server.session.Options()
+	modified := original
+	modified.SymbolMatcher = matcher
+	r.server.session.SetOptions(modified)
+	defer r.server.session.SetOptions(original)
 
 	params := &protocol.WorkspaceSymbolParams{
 		Query: query,
 	}
-	symbols, err := r.server.Symbol(r.ctx, params)
+	got, err := r.server.Symbol(r.ctx, params)
 	if err != nil {
 		t.Fatal(err)
 	}
-	return symbols
+	got = tests.FilterWorkspaceSymbols(got, dirs)
+	if len(got) != len(expectedSymbols) {
+		t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
+		return
+	}
+	if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
+		t.Error(diff)
+	}
 }
 
 func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.SignatureHelp) {
diff --git a/internal/lsp/lsprpc/lsprpc_test.go b/internal/lsp/lsprpc/lsprpc_test.go
index 7540133..c89e54c 100644
--- a/internal/lsp/lsprpc/lsprpc_test.go
+++ b/internal/lsp/lsprpc/lsprpc_test.go
@@ -218,13 +218,13 @@
 	tsForwarder := servertest.NewPipeServer(clientCtx, forwarder)
 
 	conn1 := tsForwarder.Connect(clientCtx)
-	ed1, err := fake.NewEditor(sb).Connect(clientCtx, conn1)
+	ed1, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(clientCtx, conn1, fake.ClientHooks{})
 	if err != nil {
 		t.Fatal(err)
 	}
 	defer ed1.Shutdown(clientCtx)
 	conn2 := tsBackend.Connect(baseCtx)
-	ed2, err := fake.NewEditor(sb).Connect(baseCtx, conn2)
+	ed2, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(baseCtx, conn2, fake.ClientHooks{})
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/internal/lsp/mod/code_lens.go b/internal/lsp/mod/code_lens.go
index 3c4ada1..39ca193 100644
--- a/internal/lsp/mod/code_lens.go
+++ b/internal/lsp/mod/code_lens.go
@@ -13,7 +13,11 @@
 	"golang.org/x/tools/internal/span"
 )
 
+// CodeLens computes code lens for a go.mod file.
 func CodeLens(ctx context.Context, snapshot source.Snapshot, uri span.URI) ([]protocol.CodeLens, error) {
+	if !snapshot.View().Options().EnabledCodeLens[source.CommandUpgradeDependency] {
+		return nil, nil
+	}
 	realURI, _ := snapshot.View().ModFiles()
 	if realURI == "" {
 		return nil, nil
@@ -50,7 +54,7 @@
 			Range: rng,
 			Command: protocol.Command{
 				Title:     fmt.Sprintf("Upgrade dependency to %s", latest),
-				Command:   "upgrade.dependency",
+				Command:   source.CommandUpgradeDependency,
 				Arguments: []interface{}{uri, dep},
 			},
 		})
@@ -67,7 +71,7 @@
 			Range: rng,
 			Command: protocol.Command{
 				Title:     "Upgrade all dependencies",
-				Command:   "upgrade.dependency",
+				Command:   source.CommandUpgradeDependency,
 				Arguments: []interface{}{uri, strings.Join(append([]string{"-u"}, allUpgrades...), " ")},
 			},
 		})
diff --git a/internal/lsp/protocol/context.go b/internal/lsp/protocol/context.go
index 5feeb34..5a87dd2 100644
--- a/internal/lsp/protocol/context.go
+++ b/internal/lsp/protocol/context.go
@@ -1,11 +1,12 @@
 package protocol
 
 import (
+	"bytes"
 	"context"
-	"fmt"
 
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/event/core"
+	"golang.org/x/tools/internal/event/export"
 	"golang.org/x/tools/internal/event/label"
 	"golang.org/x/tools/internal/xcontext"
 )
@@ -28,7 +29,10 @@
 	if !ok {
 		return ctx
 	}
-	msg := &LogMessageParams{Type: Info, Message: fmt.Sprint(ev)}
+	buf := &bytes.Buffer{}
+	p := export.Printer{}
+	p.WriteEvent(buf, ev, tags)
+	msg := &LogMessageParams{Type: Info, Message: buf.String()}
 	if event.IsError(ev) {
 		msg.Type = Error
 	}
diff --git a/internal/lsp/regtest/codelens_test.go b/internal/lsp/regtest/codelens_test.go
new file mode 100644
index 0000000..9bc913f
--- /dev/null
+++ b/internal/lsp/regtest/codelens_test.go
@@ -0,0 +1,57 @@
+// 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/fake"
+	"golang.org/x/tools/internal/lsp/source"
+)
+
+func TestDisablingCodeLens(t *testing.T) {
+	const workspace = `
+-- go.mod --
+module codelens.test
+-- lib.go --
+package lib
+
+type Number int
+
+const (
+	Zero Number = iota
+	One
+	Two
+)
+
+//go:generate stringer -type=Number
+`
+	tests := []struct {
+		label        string
+		enabled      map[string]bool
+		wantCodeLens bool
+	}{
+		{
+			label:        "default",
+			wantCodeLens: true,
+		},
+		{
+			label:        "generate disabled",
+			enabled:      map[string]bool{source.CommandGenerate: false},
+			wantCodeLens: false,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.label, func(t *testing.T) {
+			runner.Run(t, workspace, func(t *testing.T, env *Env) {
+				env.OpenFile("lib.go")
+				lens := env.CodeLens("lib.go")
+				if gotCodeLens := len(lens) > 0; gotCodeLens != test.wantCodeLens {
+					t.Errorf("got codeLens: %t, want %t", gotCodeLens, test.wantCodeLens)
+				}
+			}, WithEditorConfig(fake.EditorConfig{CodeLens: test.enabled}))
+		})
+	}
+}
diff --git a/internal/lsp/regtest/diagnostics_test.go b/internal/lsp/regtest/diagnostics_test.go
index f6d3dc4..6a5f016 100644
--- a/internal/lsp/regtest/diagnostics_test.go
+++ b/internal/lsp/regtest/diagnostics_test.go
@@ -382,7 +382,7 @@
 		if err := env.Editor.OrganizeImports(env.Ctx, "main.go"); err == nil {
 			t.Fatalf("organize imports should fail with an empty GOPATH")
 		}
-	}, WithEnv("GOPATH="))
+	}, WithEditorConfig(fake.EditorConfig{Env: []string{"GOPATH="}}))
 }
 
 // Tests golang/go#38669.
@@ -404,7 +404,7 @@
 		env.OpenFile("main.go")
 		env.OrganizeImports("main.go")
 		env.Await(EmptyDiagnostics("main.go"))
-	}, WithEnv("GOFLAGS=-tags=foo"))
+	}, WithEditorConfig(fake.EditorConfig{Env: []string{"GOFLAGS=-tags=foo"}}))
 }
 
 // Tests golang/go#38467.
diff --git a/internal/lsp/regtest/env.go b/internal/lsp/regtest/env.go
index a9749a2..08fc842 100644
--- a/internal/lsp/regtest/env.go
+++ b/internal/lsp/regtest/env.go
@@ -102,18 +102,13 @@
 
 // NewEnv creates a new test environment using the given scratch environment
 // and gopls server.
-func NewEnv(ctx context.Context, t *testing.T, scratch *fake.Sandbox, ts servertest.Connector) *Env {
+func NewEnv(ctx context.Context, t *testing.T, scratch *fake.Sandbox, ts servertest.Connector, editorConfig fake.EditorConfig) *Env {
 	t.Helper()
 	conn := ts.Connect(ctx)
-	editor, err := fake.NewEditor(scratch).Connect(ctx, conn)
-	if err != nil {
-		t.Fatal(err)
-	}
 	env := &Env{
 		T:       t,
 		Ctx:     ctx,
 		Sandbox: scratch,
-		Editor:  editor,
 		Server:  ts,
 		Conn:    conn,
 		state: State{
@@ -123,11 +118,18 @@
 		},
 		waiters: make(map[int]*condition),
 	}
-	env.Editor.Client().OnDiagnostics(env.onDiagnostics)
-	env.Editor.Client().OnLogMessage(env.onLogMessage)
-	env.Editor.Client().OnWorkDoneProgressCreate(env.onWorkDoneProgressCreate)
-	env.Editor.Client().OnProgress(env.onProgress)
-	env.Editor.Client().OnShowMessage(env.onShowMessage)
+	hooks := fake.ClientHooks{
+		OnDiagnostics:            env.onDiagnostics,
+		OnLogMessage:             env.onLogMessage,
+		OnWorkDoneProgressCreate: env.onWorkDoneProgressCreate,
+		OnProgress:               env.onProgress,
+		OnShowMessage:            env.onShowMessage,
+	}
+	editor, err := fake.NewEditor(scratch, editorConfig).Connect(ctx, conn, hooks)
+	if err != nil {
+		t.Fatal(err)
+	}
+	env.Editor = editor
 	return env
 }
 
diff --git a/internal/lsp/regtest/runner.go b/internal/lsp/regtest/runner.go
index 557104b..3d838fa 100644
--- a/internal/lsp/regtest/runner.go
+++ b/internal/lsp/regtest/runner.go
@@ -66,12 +66,12 @@
 }
 
 type runConfig struct {
-	modes       Mode
-	proxyTxt    string
-	timeout     time.Duration
-	env         []string
-	skipCleanup bool
-	gopath      bool
+	editorConfig fake.EditorConfig
+	modes        Mode
+	proxyTxt     string
+	timeout      time.Duration
+	skipCleanup  bool
+	gopath       bool
 }
 
 func (r *Runner) defaultConfig() *runConfig {
@@ -113,11 +113,10 @@
 	})
 }
 
-// WithEnv overlays environment variables encoded by "<var>=<value" on top of
-// the default regtest environment.
-func WithEnv(env ...string) RunOption {
+// WithEditorConfig configures the editors LSP session.
+func WithEditorConfig(config fake.EditorConfig) RunOption {
 	return optionSetter(func(opts *runConfig) {
-		opts.env = env
+		opts.editorConfig = config
 	})
 }
 
@@ -168,7 +167,7 @@
 			defer cancel()
 			ctx = debug.WithInstance(ctx, "", "")
 
-			sandbox, err := fake.NewSandbox("regtest", filedata, config.proxyTxt, config.gopath, config.env...)
+			sandbox, err := fake.NewSandbox("regtest", filedata, config.proxyTxt, config.gopath)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -191,7 +190,7 @@
 			defer func() {
 				ts.Close()
 			}()
-			env := NewEnv(ctx, t, sandbox, ts)
+			env := NewEnv(ctx, t, sandbox, ts, config.editorConfig)
 			defer func() {
 				if t.Failed() && r.PrintGoroutinesOnFailure {
 					pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
diff --git a/internal/lsp/regtest/shared_test.go b/internal/lsp/regtest/shared_test.go
index c4e1840..ea16f14 100644
--- a/internal/lsp/regtest/shared_test.go
+++ b/internal/lsp/regtest/shared_test.go
@@ -28,7 +28,7 @@
 	runner.Run(t, sharedProgram, func(t *testing.T, env1 *Env) {
 		// Create a second test session connected to the same workspace and server
 		// as the first.
-		env2 := NewEnv(env1.Ctx, t, env1.Sandbox, env1.Server)
+		env2 := NewEnv(env1.Ctx, t, env1.Sandbox, env1.Server, env1.Editor.Config)
 		testFunc(env1, env2)
 	}, WithModes(modes))
 }
diff --git a/internal/lsp/regtest/unix_test.go b/internal/lsp/regtest/unix_test.go
index 94d7bfb..955bb5a 100644
--- a/internal/lsp/regtest/unix_test.go
+++ b/internal/lsp/regtest/unix_test.go
@@ -9,6 +9,8 @@
 import (
 	"fmt"
 	"testing"
+
+	"golang.org/x/tools/internal/lsp/fake"
 )
 
 func TestBadGOPATH(t *testing.T) {
@@ -28,5 +30,7 @@
 		if err := env.Editor.OrganizeImports(env.Ctx, "main.go"); err != nil {
 			t.Fatal(err)
 		}
-	}, WithEnv(fmt.Sprintf("GOPATH=:/path/to/gopath")))
+	}, WithEditorConfig(fake.EditorConfig{
+		Env: []string{fmt.Sprintf("GOPATH=:/path/to/gopath")},
+	}))
 }
diff --git a/internal/lsp/regtest/wrappers.go b/internal/lsp/regtest/wrappers.go
index 93131fd..4464f43 100644
--- a/internal/lsp/regtest/wrappers.go
+++ b/internal/lsp/regtest/wrappers.go
@@ -166,3 +166,14 @@
 		e.T.Fatal(err)
 	}
 }
+
+// CodeLens calls textDocument/codeLens for the given path, calling t.Fatal on
+// any error.
+func (e *Env) CodeLens(path string) []protocol.CodeLens {
+	e.T.Helper()
+	lens, err := e.Editor.CodeLens(e.Ctx, path)
+	if err != nil {
+		e.T.Fatal(err)
+	}
+	return lens
+}
diff --git a/internal/lsp/source/code_lens.go b/internal/lsp/source/code_lens.go
index 6bdda9e..689a6fe 100644
--- a/internal/lsp/source/code_lens.go
+++ b/internal/lsp/source/code_lens.go
@@ -13,7 +13,11 @@
 	"golang.org/x/tools/internal/lsp/protocol"
 )
 
+// CodeLens computes code lens for Go source code.
 func CodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
+	if !snapshot.View().Options().EnabledCodeLens[CommandGenerate] {
+		return nil, nil
+	}
 	f, _, m, _, err := snapshot.View().Session().Cache().ParseGoHandle(fh, ParseFull).Parse(ctx)
 	if err != nil {
 		return nil, err
@@ -35,7 +39,7 @@
 					Range: rng,
 					Command: protocol.Command{
 						Title:     "run go generate",
-						Command:   "generate",
+						Command:   CommandGenerate,
 						Arguments: []interface{}{dir, false},
 					},
 				},
@@ -43,7 +47,7 @@
 					Range: rng,
 					Command: protocol.Command{
 						Title:     "run go generate ./...",
-						Command:   "generate",
+						Command:   CommandGenerate,
 						Arguments: []interface{}{dir, true},
 					},
 				},
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index fff371a..fe3ba16 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -455,6 +455,7 @@
 
 	pos := rng.Start
 
+	// Check if completion at this position is valid. If not, return early.
 	switch n := path[0].(type) {
 	case *ast.BasicLit:
 		// Skip completion inside any kind of literal.
@@ -465,6 +466,20 @@
 			// example, don't offer completions at "<>" in "foo(bar...<>").
 			return nil, nil, nil
 		}
+	case *ast.Ident:
+		// reject defining identifiers
+		if obj, ok := pkg.GetTypesInfo().Defs[n]; ok {
+			if v, ok := obj.(*types.Var); ok && v.IsField() && v.Embedded() {
+				// An anonymous field is also a reference to a type.
+			} else {
+				objStr := ""
+				if obj != nil {
+					qual := types.RelativeTo(pkg.GetTypes())
+					objStr = types.ObjectString(obj, qual)
+				}
+				return nil, nil, ErrIsDefinition{objStr: objStr}
+			}
+		}
 	}
 
 	opts := snapshot.View().Options()
@@ -557,19 +572,6 @@
 			}
 			return c.items, c.getSurrounding(), nil
 		}
-		// reject defining identifiers
-		if obj, ok := pkg.GetTypesInfo().Defs[n]; ok {
-			if v, ok := obj.(*types.Var); ok && v.IsField() && v.Embedded() {
-				// An anonymous field is also a reference to a type.
-			} else {
-				objStr := ""
-				if obj != nil {
-					qual := types.RelativeTo(pkg.GetTypes())
-					objStr = types.ObjectString(obj, qual)
-				}
-				return nil, nil, ErrIsDefinition{objStr: objStr}
-			}
-		}
 		if err := c.lexical(ctx); err != nil {
 			return nil, nil, err
 		}
diff --git a/internal/lsp/source/identifier.go b/internal/lsp/source/identifier.go
index 2c2266c..85ca19f 100644
--- a/internal/lsp/source/identifier.go
+++ b/internal/lsp/source/identifier.go
@@ -103,6 +103,10 @@
 				declAST = f
 			}
 		}
+		// If there's no package documentation, just use current file.
+		if declAST == nil {
+			declAST = file
+		}
 		declRng, err := posToMappedRange(view, pkg, declAST.Name.Pos(), declAST.Name.End())
 		if err != nil {
 			return nil, err
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index b51b3a0..363dcc5 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -52,6 +52,18 @@
 	errors "golang.org/x/xerrors"
 )
 
+const (
+	// CommandGenerate is a gopls command to run `go generate` for a directory.
+	CommandGenerate = "generate"
+	// CommandTidy is a gopls command to run `go mod tidy` for a module.
+	CommandTidy = "tidy"
+	// CommandUpgradeDependency is a gopls command to upgrade a dependency.
+	CommandUpgradeDependency = "upgrade.dependency"
+)
+
+// DefaultOptions is the options that are used for Gopls execution independent
+// of any externally provided configuration (LSP initialization, command
+// invokation, etc.).
 func DefaultOptions() Options {
 	return Options{
 		ClientOptions: ClientOptions{
@@ -76,9 +88,9 @@
 				Sum: {},
 			},
 			SupportedCommands: []string{
-				"tidy",               // for go.mod files
-				"upgrade.dependency", // for go.mod dependency upgrades
-				"generate",           // for "go generate" commands
+				CommandTidy,              // for go.mod files
+				CommandUpgradeDependency, // for go.mod dependency upgrades
+				CommandGenerate,          // for "go generate" commands
 			},
 		},
 		UserOptions: UserOptions{
@@ -86,9 +98,14 @@
 			HoverKind:               FullDocumentation,
 			LinkTarget:              "pkg.go.dev",
 			Matcher:                 Fuzzy,
+			SymbolMatcher:           SymbolFuzzy,
 			DeepCompletion:          true,
 			UnimportedCompletion:    true,
 			CompletionDocumentation: true,
+			EnabledCodeLens: map[string]bool{
+				CommandGenerate:          true,
+				CommandUpgradeDependency: true,
+			},
 		},
 		DebuggingOptions: DebuggingOptions{
 			CompletionBudget: 100 * time.Millisecond,
@@ -106,6 +123,8 @@
 	}
 }
 
+// Options holds various configuration that affects Gopls execution, organized
+// by the nature or origin of the settings.
 type Options struct {
 	ClientOptions
 	ServerOptions
@@ -115,6 +134,8 @@
 	Hooks
 }
 
+// ClientOptions holds LSP-specific configuration that is provided by the
+// client.
 type ClientOptions struct {
 	InsertTextFormat                  protocol.InsertTextFormat
 	ConfigurationSupported            bool
@@ -125,11 +146,15 @@
 	HierarchicalDocumentSymbolSupport bool
 }
 
+// ServerOptions holds LSP-specific configuration that is provided by the
+// server.
 type ServerOptions struct {
 	SupportedCodeActions map[FileKind]map[protocol.CodeActionKind]bool
 	SupportedCommands    []string
 }
 
+// UserOptions holds custom Gopls configuration (not part of the LSP) that is
+// modified by the client.
 type UserOptions struct {
 	// Env is the current set of environment overrides on this view.
 	Env []string
@@ -140,9 +165,10 @@
 	// HoverKind specifies the format of the content for hover requests.
 	HoverKind HoverKind
 
-	// UserEnabledAnalyses specify analyses that the user would like to enable or disable.
-	// A map of the names of analysis passes that should be enabled/disabled.
-	// A full list of analyzers that gopls uses can be found [here](analyzers.md)
+	// UserEnabledAnalyses specifies analyses that the user would like to enable
+	// or disable. A map of the names of analysis passes that should be
+	// enabled/disabled. A full list of analyzers that gopls uses can be found
+	// [here](analyzers.md).
 	//
 	// Example Usage:
 	// ...
@@ -152,6 +178,10 @@
 	// }
 	UserEnabledAnalyses map[string]bool
 
+	// EnabledCodeLens specifies which codelens are enabled, keyed by the gopls
+	// command that they provide.
+	EnabledCodeLens map[string]bool
+
 	// StaticCheck enables additional analyses from staticcheck.io.
 	StaticCheck bool
 
@@ -164,6 +194,9 @@
 	// Matcher specifies the type of matcher to use for completion requests.
 	Matcher Matcher
 
+	// SymbolMatcher specifies the type of matcher to use for symbol requests.
+	SymbolMatcher SymbolMatcher
+
 	// DeepCompletion allows completion to perform nested searches through
 	// possible candidates.
 	DeepCompletion bool
@@ -191,6 +224,8 @@
 	budget            time.Duration
 }
 
+// Hooks contains configuration that is provided to the Gopls command by the
+// main package.
 type Hooks struct {
 	GoDiff             bool
 	ComputeEdits       diff.ComputeEdits
@@ -215,6 +250,8 @@
 	VerboseWorkDoneProgress bool
 }
 
+// DebuggingOptions should not affect the logical execution of Gopls, but may
+// be altered for debugging purposes.
 type DebuggingOptions struct {
 	VerboseOutput bool
 
@@ -234,6 +271,14 @@
 	CaseSensitive
 )
 
+type SymbolMatcher int
+
+const (
+	SymbolFuzzy = SymbolMatcher(iota)
+	SymbolCaseInsensitive
+	SymbolCaseSensitive
+)
+
 type HoverKind int
 
 const (
@@ -366,6 +411,20 @@
 			o.Matcher = CaseInsensitive
 		}
 
+	case "symbolMatcher":
+		matcher, ok := result.asString()
+		if !ok {
+			break
+		}
+		switch matcher {
+		case "fuzzy":
+			o.SymbolMatcher = SymbolFuzzy
+		case "caseSensitive":
+			o.SymbolMatcher = SymbolCaseSensitive
+		default:
+			o.SymbolMatcher = SymbolCaseInsensitive
+		}
+
 	case "hoverKind":
 		hoverKind, ok := result.asString()
 		if !ok {
@@ -387,23 +446,20 @@
 		}
 
 	case "linkTarget":
-		linkTarget, ok := value.(string)
-		if !ok {
-			result.errorf("invalid type %T for string option %q", value, name)
-			break
-		}
-		o.LinkTarget = linkTarget
+		result.setString(&o.LinkTarget)
 
 	case "analyses":
-		allAnalyses, ok := value.(map[string]interface{})
-		if !ok {
-			result.errorf("Invalid type %T for map[string]interface{} option %q", value, name)
-			break
-		}
-		o.UserEnabledAnalyses = make(map[string]bool)
-		for a, enabled := range allAnalyses {
-			if enabled, ok := enabled.(bool); ok {
-				o.UserEnabledAnalyses[a] = enabled
+		result.setBoolMap(&o.UserEnabledAnalyses)
+
+	case "codelens":
+		var lensOverrides map[string]bool
+		result.setBoolMap(&lensOverrides)
+		if result.Error == nil {
+			if o.EnabledCodeLens == nil {
+				o.EnabledCodeLens = make(map[string]bool)
+			}
+			for lens, enabled := range lensOverrides {
+				o.EnabledCodeLens[lens] = enabled
 			}
 		}
 
@@ -411,12 +467,7 @@
 		result.setBool(&o.StaticCheck)
 
 	case "local":
-		localPrefix, ok := value.(string)
-		if !ok {
-			result.errorf("invalid type %T for string option %q", value, name)
-			break
-		}
-		o.LocalPrefix = localPrefix
+		result.setString(&o.LocalPrefix)
 
 	case "verboseOutput":
 		result.setBool(&o.VerboseOutput)
@@ -488,6 +539,30 @@
 	return b, true
 }
 
+func (r *OptionResult) setBool(b *bool) {
+	if v, ok := r.asBool(); ok {
+		*b = v
+	}
+}
+
+func (r *OptionResult) setBoolMap(bm *map[string]bool) {
+	all, ok := r.Value.(map[string]interface{})
+	if !ok {
+		r.errorf("Invalid type %T for map[string]interface{} option %q", r.Value, r.Name)
+		return
+	}
+	m := make(map[string]bool)
+	for a, enabled := range all {
+		if enabled, ok := enabled.(bool); ok {
+			m[a] = enabled
+		} else {
+			r.errorf("Invalid type %d for map key %q in option %q", a, r.Name)
+			return
+		}
+	}
+	*bm = m
+}
+
 func (r *OptionResult) asString() (string, bool) {
 	b, ok := r.Value.(string)
 	if !ok {
@@ -497,9 +572,9 @@
 	return b, true
 }
 
-func (r *OptionResult) setBool(b *bool) {
-	if v, ok := r.asBool(); ok {
-		*b = v
+func (r *OptionResult) setString(s *string) {
+	if v, ok := r.asString(); ok {
+		*s = v
 	}
 }
 
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index 5053721..219388f 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -805,37 +805,23 @@
 }
 
 func (r *runner) WorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
-	got := r.callWorkspaceSymbols(t, query, func(opts *source.Options) {
-		opts.Matcher = source.CaseInsensitive
-	})
-	got = tests.FilterWorkspaceSymbols(got, dirs)
-	if len(got) != len(expectedSymbols) {
-		t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
-		return
-	}
-	if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
-		t.Error(diff)
-	}
+	r.callWorkspaceSymbols(t, query, source.SymbolCaseInsensitive, dirs, expectedSymbols)
 }
 
 func (r *runner) FuzzyWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
-	got := r.callWorkspaceSymbols(t, query, func(opts *source.Options) {
-		opts.Matcher = source.Fuzzy
-	})
-	got = tests.FilterWorkspaceSymbols(got, dirs)
-	if len(got) != len(expectedSymbols) {
-		t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
-		return
-	}
-	if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
-		t.Error(diff)
-	}
+	r.callWorkspaceSymbols(t, query, source.SymbolFuzzy, dirs, expectedSymbols)
 }
 
 func (r *runner) CaseSensitiveWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
-	got := r.callWorkspaceSymbols(t, query, func(opts *source.Options) {
-		opts.Matcher = source.CaseSensitive
-	})
+	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) {
+	t.Helper()
+	got, err := source.WorkspaceSymbols(r.ctx, matcher, []source.View{r.view}, query)
+	if err != nil {
+		t.Fatal(err)
+	}
 	got = tests.FilterWorkspaceSymbols(got, dirs)
 	if len(got) != len(expectedSymbols) {
 		t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
@@ -846,25 +832,6 @@
 	}
 }
 
-func (r *runner) callWorkspaceSymbols(t *testing.T, query string, options func(*source.Options)) []protocol.SymbolInformation {
-	t.Helper()
-
-	original := r.view.Options()
-	modified := original
-	options(&modified)
-	view, err := r.view.SetOptions(r.ctx, modified)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer r.view.SetOptions(r.ctx, original)
-
-	got, err := source.WorkspaceSymbols(r.ctx, []source.View{view}, query)
-	if err != nil {
-		t.Fatal(err)
-	}
-	return got
-}
-
 func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.SignatureHelp) {
 	_, rng, err := spanToRange(r.data, spn)
 	if err != nil {
diff --git a/internal/lsp/source/types_format.go b/internal/lsp/source/types_format.go
index 2a9e0ec..b9e8955 100644
--- a/internal/lsp/source/types_format.go
+++ b/internal/lsp/source/types_format.go
@@ -386,8 +386,8 @@
 			if obj, ok := info.ObjectOf(x).(*types.PkgName); ok {
 				clonedInfo[s.X.Pos()] = obj
 			}
-
 		}
+		return s
 	case *ast.StarExpr:
 		return &ast.StarExpr{
 			Star: expr.Star,
@@ -399,8 +399,9 @@
 			Fields:     cloneFieldList(expr.Fields, info, clonedInfo),
 			Incomplete: expr.Incomplete,
 		}
+	default:
+		return expr
 	}
-	return expr
 }
 
 func cloneFieldList(fl *ast.FieldList, info *types.Info, clonedInfo map[token.Pos]*types.PkgName) *ast.FieldList {
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 29b3671..e204bd1 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -185,6 +185,9 @@
 	// It returns the resulting snapshots, a guaranteed one per view.
 	DidModifyFiles(ctx context.Context, changes []FileModification) ([]Snapshot, error)
 
+	// UnsavedFiles returns a slice of open but unsaved files in the session.
+	UnsavedFiles() []span.URI
+
 	// Options returns a copy of the SessionOptions for this session.
 	Options() Options
 
@@ -357,9 +360,13 @@
 type FileKind int
 
 const (
+	// Go is a normal go source file.
 	Go = FileKind(iota)
+	// Mod is a go.mod file.
 	Mod
+	// Sum is a go.sum file.
 	Sum
+	// UnknownKind is a file type we don't know about.
 	UnknownKind
 )
 
diff --git a/internal/lsp/source/workspace_symbol.go b/internal/lsp/source/workspace_symbol.go
index ae40b4a..b4d98b4 100644
--- a/internal/lsp/source/workspace_symbol.go
+++ b/internal/lsp/source/workspace_symbol.go
@@ -18,10 +18,31 @@
 
 const maxSymbols = 100
 
-func WorkspaceSymbols(ctx context.Context, views []View, query string) ([]protocol.SymbolInformation, error) {
+// WorkspaceSymbols matches symbols across views using the given query,
+// according to the SymbolMatcher matcher.
+//
+// The workspace symbol method is defined in the spec as follows:
+//
+//  > The workspace symbol request is sent from the client to the server to
+//  > list project-wide symbols matching the query string.
+//
+// It is unclear what "project-wide" means here, but given the parameters of
+// workspace/symbol do not include any workspace identifier, then it has to be
+// assumed that "project-wide" means "across all workspaces".  Hence why
+// WorkspaceSymbols receives the views []View.
+//
+// However, it then becomes unclear what it would mean to call WorkspaceSymbols
+// 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, views []View, query string) ([]protocol.SymbolInformation, error) {
 	ctx, done := event.Start(ctx, "source.WorkspaceSymbols")
 	defer done()
+	if query == "" {
+		return nil, nil
+	}
 
+	matcher := makeMatcher(matcherType, query)
 	seen := make(map[string]struct{})
 	var symbols []protocol.SymbolInformation
 outer:
@@ -30,7 +51,6 @@
 		if err != nil {
 			return nil, err
 		}
-		matcher := makeMatcher(view.Options().Matcher, query)
 		for _, ph := range knownPkgs {
 			pkg, err := ph.Check(ctx)
 			if err != nil {
@@ -82,14 +102,14 @@
 
 type matcherFunc func(string) bool
 
-func makeMatcher(m Matcher, query string) matcherFunc {
+func makeMatcher(m SymbolMatcher, query string) matcherFunc {
 	switch m {
-	case Fuzzy:
+	case SymbolFuzzy:
 		fm := fuzzy.NewMatcher(query)
 		return func(s string) bool {
 			return fm.Score(s) > 0
 		}
-	case CaseSensitive:
+	case SymbolCaseSensitive:
 		return func(s string) bool {
 			return strings.Contains(s, query)
 		}
diff --git a/internal/lsp/workspace_symbol.go b/internal/lsp/workspace_symbol.go
index 66282a2..a233d44 100644
--- a/internal/lsp/workspace_symbol.go
+++ b/internal/lsp/workspace_symbol.go
@@ -16,5 +16,7 @@
 	ctx, done := event.Start(ctx, "lsp.Server.symbol")
 	defer done()
 
-	return source.WorkspaceSymbols(ctx, s.session.Views(), params.Query)
+	views := s.session.Views()
+	matcher := s.session.Options().SymbolMatcher
+	return source.WorkspaceSymbols(ctx, matcher, views, params.Query)
 }
diff --git a/internal/testenv/testenv.go b/internal/testenv/testenv.go
index 0cc90d2..6c5fb98 100644
--- a/internal/testenv/testenv.go
+++ b/internal/testenv/testenv.go
@@ -7,6 +7,7 @@
 package testenv
 
 import (
+	"bytes"
 	"fmt"
 	"io/ioutil"
 	"os"
@@ -77,6 +78,17 @@
 		if checkGoGoroot.err != nil {
 			return checkGoGoroot.err
 		}
+
+	case "diff":
+		// Check that diff is the GNU version, needed for the -u argument and
+		// to report missing newlines at the end of files.
+		out, err := exec.Command(tool, "-version").Output()
+		if err != nil {
+			return err
+		}
+		if !bytes.Contains(out, []byte("GNU diffutils")) {
+			return fmt.Errorf("diff is not the GNU version")
+		}
 	}
 
 	return nil
@@ -178,8 +190,12 @@
 //
 // It should be called from within a TestMain function.
 func ExitIfSmallMachine() {
-	if os.Getenv("GO_BUILDER_NAME") == "linux-arm" {
+	switch os.Getenv("GO_BUILDER_NAME") {
+	case "linux-arm":
 		fmt.Fprintln(os.Stderr, "skipping test: linux-arm builder lacks sufficient memory (https://golang.org/issue/32834)")
 		os.Exit(0)
+	case "plan9-arm":
+		fmt.Fprintln(os.Stderr, "skipping test: plan9-arm builder lacks sufficient memory (https://golang.org/issue/38772)")
+		os.Exit(0)
 	}
 }