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)
}
}