diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index c180f70bbe..cb9983ee75 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -244,6 +244,16 @@ their expected values. Default: `true`. +### **experimentalDiagnosticsDelay** *time.Duration* +experimentalDiagnosticsDelay controls the amount of time that gopls waits +after the most recent file modification before computing deep diagnostics. +Simple diagnostics (parsing and type-checking) are always run immediately +on recently modified packages. + +This option must be set to a valid duration string, for example `"250ms"`. + + +Default: `0`. ## Debugging diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 01ec4ec337..66a61650a5 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -57,7 +57,12 @@ func (s *snapshot) load(ctx context.Context, scopes ...interface{}) error { // go list and should already be GOPATH-vendorized when appropriate. query = append(query, string(scope)) case fileURI: - query = append(query, fmt.Sprintf("file=%s", span.URI(scope).Filename())) + uri := span.URI(scope) + // Don't try to load a file that doesn't exist. + fh := s.FindFile(uri) + if fh != nil { + query = append(query, fmt.Sprintf("file=%s", uri.Filename())) + } case moduleLoadScope: query = append(query, fmt.Sprintf("%s/...", scope)) case viewLoadScope: @@ -76,6 +81,9 @@ func (s *snapshot) load(ctx context.Context, scopes ...interface{}) error { containsDir = true } } + if len(query) == 0 { + return nil + } sort.Strings(query) // for determinism ctx, done := event.Start(ctx, "cache.view.load", tag.Query.Of(query)) diff --git a/internal/lsp/command.go b/internal/lsp/command.go index 23f50ca874..546372f959 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -217,7 +217,7 @@ func (s *Server) runCommand(ctx context.Context, work *workDone, command *source } snapshot, release := sv.Snapshot(ctx) defer release() - s.diagnoseSnapshot(snapshot) + s.diagnoseSnapshot(snapshot, nil) case source.CommandGenerateGoplsMod: var v source.View if len(args) == 0 { diff --git a/internal/lsp/debounce.go b/internal/lsp/debounce.go new file mode 100644 index 0000000000..80cf78b48e --- /dev/null +++ b/internal/lsp/debounce.go @@ -0,0 +1,81 @@ +// 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 lsp + +import ( + "sync" + "time" +) + +type debounceFunc struct { + order uint64 + done chan struct{} +} + +type debouncer struct { + mu sync.Mutex + funcs map[string]*debounceFunc +} + +func newDebouncer() *debouncer { + return &debouncer{ + funcs: make(map[string]*debounceFunc), + } +} + +// debounce waits timeout before running f, if no subsequent call is made with +// the same key in the intervening time. If a later call to debounce with the +// same key occurs while the original call is blocking, the original call will +// return immediately without running its f. +// +// If order is specified, it will be used to order calls logically, so calls +// with lesser order will not cancel calls with greater order. +func (d *debouncer) debounce(key string, order uint64, timeout time.Duration, f func()) { + if timeout == 0 { + // Degenerate case: no debouncing. + f() + return + } + + // First, atomically acquire the current func, cancel it, and insert this + // call into d.funcs. + d.mu.Lock() + current, ok := d.funcs[key] + if ok && current.order > order { + // If we have a logical ordering of events (as is the case for snapshots), + // don't overwrite a later event with an earlier event. + d.mu.Unlock() + return + } + if ok { + close(current.done) + } + done := make(chan struct{}) + next := &debounceFunc{ + order: order, + done: done, + } + d.funcs[key] = next + d.mu.Unlock() + + // Next, wait to be cancelled or for our wait to expire. There is a race here + // that we must handle: our timer could expire while another goroutine holds + // d.mu. + select { + case <-done: + case <-time.After(timeout): + d.mu.Lock() + if d.funcs[key] != next { + // We lost the race: another event has arrived for the key and started + // waiting. We could reasonably choose to run f at this point, but doing + // nothing is simpler. + d.mu.Unlock() + return + } + delete(d.funcs, key) + d.mu.Unlock() + f() + } +} diff --git a/internal/lsp/debounce_test.go b/internal/lsp/debounce_test.go new file mode 100644 index 0000000000..a06af3584d --- /dev/null +++ b/internal/lsp/debounce_test.go @@ -0,0 +1,87 @@ +// 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 lsp + +import ( + "sync" + "testing" + "time" +) + +func TestDebouncer(t *testing.T) { + t.Parallel() + type event struct { + key string + order uint64 + fired bool + wantFired bool + } + tests := []struct { + label string + events []*event + }{ + { + label: "overridden", + events: []*event{ + {key: "a", order: 1, wantFired: false}, + {key: "a", order: 2, wantFired: true}, + }, + }, + { + label: "distinct labels", + events: []*event{ + {key: "a", order: 1, wantFired: true}, + {key: "b", order: 2, wantFired: true}, + }, + }, + { + label: "reverse order", + events: []*event{ + {key: "a", order: 2, wantFired: true}, + {key: "a", order: 1, wantFired: false}, + }, + }, + { + label: "multiple overrides", + events: []*event{ + {key: "a", order: 1, wantFired: false}, + {key: "a", order: 2, wantFired: false}, + {key: "a", order: 3, wantFired: false}, + {key: "a", order: 4, wantFired: false}, + {key: "a", order: 5, wantFired: true}, + }, + }, + } + for _, test := range tests { + test := test + t.Run(test.label, func(t *testing.T) { + t.Parallel() + d := newDebouncer() + var wg sync.WaitGroup + for i, e := range test.events { + wg.Add(1) + go func(e *event) { + d.debounce(e.key, e.order, 100*time.Millisecond, func() { + e.fired = true + }) + wg.Done() + }(e) + // For a bit more fidelity, sleep to try to make things actually + // execute in order. This doesn't have to be perfect, but could be done + // properly using fake timers. + if i < len(test.events)-1 { + time.Sleep(10 * time.Millisecond) + } + } + wg.Wait() + for _, event := range test.events { + if event.fired != event.wantFired { + t.Errorf("(key: %q, order: %d): fired = %t, want %t", + event.key, event.order, event.fired, event.wantFired) + } + } + }) + } +} diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go index 9c86a313e1..56e0e3dd49 100644 --- a/internal/lsp/diagnostics.go +++ b/internal/lsp/diagnostics.go @@ -30,6 +30,47 @@ type idWithAnalysis struct { withAnalysis bool } +// A reportSet collects diagnostics for publication, sorting them by file and +// de-duplicating. +type reportSet struct { + mu sync.Mutex + // lazily allocated + reports map[idWithAnalysis]map[string]*source.Diagnostic +} + +func (s *reportSet) add(id source.VersionedFileIdentity, withAnalysis bool, diags ...*source.Diagnostic) { + s.mu.Lock() + defer s.mu.Unlock() + if s.reports == nil { + s.reports = make(map[idWithAnalysis]map[string]*source.Diagnostic) + } + key := idWithAnalysis{ + id: id, + withAnalysis: withAnalysis, + } + if _, ok := s.reports[key]; !ok { + s.reports[key] = map[string]*source.Diagnostic{} + } + for _, d := range diags { + s.reports[key][diagnosticKey(d)] = d + } +} + +// diagnosticKey creates a unique identifier for a given diagnostic, since we +// cannot use source.Diagnostics as map keys. This is used to de-duplicate +// diagnostics. +func diagnosticKey(d *source.Diagnostic) string { + var tags, related string + for _, t := range d.Tags { + tags += fmt.Sprintf("%s", t) + } + for _, r := range d.Related { + related += fmt.Sprintf("%s%s%s", r.URI, r.Message, r.Range) + } + key := fmt.Sprintf("%s%s%s%s%s%s", d.Message, d.Range, d.Severity, d.Source, tags, related) + return fmt.Sprintf("%x", sha256.Sum256([]byte(key))) +} + func (s *Server) diagnoseDetached(snapshot source.Snapshot) { ctx := snapshot.View().BackgroundContext() ctx = xcontext.Detach(ctx) @@ -41,18 +82,69 @@ func (s *Server) diagnoseDetached(snapshot source.Snapshot) { s.publishReports(ctx, snapshot, reports) } -func (s *Server) diagnoseSnapshot(snapshot source.Snapshot) { +func (s *Server) diagnoseSnapshot(snapshot source.Snapshot, changedURIs []span.URI) { ctx := snapshot.View().BackgroundContext() + delay := snapshot.View().Options().ExperimentalDiagnosticsDelay + if delay > 0 { + // Experimental 2-phase diagnostics. + // + // The first phase just parses and checks packages that have been affected + // by file modifications (no analysis). + // + // The second phase does everything, and is debounced by the configured delay. + reports, err := s.diagnoseChangedFiles(ctx, snapshot, changedURIs) + if err != nil { + if !errors.Is(err, context.Canceled) { + event.Error(ctx, "diagnosing changed files", err) + } + } + s.publishReports(ctx, snapshot, reports) + s.debouncer.debounce(snapshot.View().Name(), snapshot.ID(), delay, func() { + reports, _ := s.diagnose(ctx, snapshot, false) + s.publishReports(ctx, snapshot, reports) + }) + return + } + // Ignore possible workspace configuration warnings in the normal flow. reports, _ := s.diagnose(ctx, snapshot, false) s.publishReports(ctx, snapshot, reports) } +func (s *Server) diagnoseChangedFiles(ctx context.Context, snapshot source.Snapshot, uris []span.URI) (*reportSet, error) { + ctx, done := event.Start(ctx, "Server.diagnoseChangedFiles") + defer done() + packages := make(map[source.Package]struct{}) + for _, uri := range uris { + pkgs, err := snapshot.PackagesForFile(ctx, uri, source.TypecheckWorkspace) + if err != nil { + // TODO (rFindley): we should probably do something with the error here, + // but as of now this can fail repeatedly if load fails, so can be too + // noisy to log (and we'll handle things later in the slow pass). + continue + } + for _, pkg := range pkgs { + packages[pkg] = struct{}{} + } + } + reports := new(reportSet) + for pkg := range packages { + pkgReports, _, err := source.Diagnostics(ctx, snapshot, pkg, false) + if err != nil { + return nil, err + } + for id, diags := range pkgReports { + reports.add(id, false, diags...) + } + } + return reports, nil +} + // diagnose is a helper function for running diagnostics with a given context. // Do not call it directly. -func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, alwaysAnalyze bool) (map[idWithAnalysis]map[string]*source.Diagnostic, *protocol.ShowMessageParams) { - ctx, done := event.Start(ctx, "lsp:background-worker") +func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, alwaysAnalyze bool) (diagReports *reportSet, _ *protocol.ShowMessageParams) { + ctx, done := event.Start(ctx, "Server.diagnose") defer done() // Wait for a free diagnostics slot. @@ -61,24 +153,11 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, alwaysA return nil, nil case s.diagnosticsSema <- struct{}{}: } - defer func() { <-s.diagnosticsSema }() + defer func() { + <-s.diagnosticsSema + }() - var reportsMu sync.Mutex - reports := map[idWithAnalysis]map[string]*source.Diagnostic{} - addReport := func(id source.VersionedFileIdentity, withAnalysis bool, diags []*source.Diagnostic) { - reportsMu.Lock() - defer reportsMu.Unlock() - key := idWithAnalysis{ - id: id, - withAnalysis: withAnalysis, - } - if _, ok := reports[key]; !ok { - reports[key] = map[string]*source.Diagnostic{} - } - for _, d := range diags { - reports[key][diagnosticKey(d)] = d - } - } + reports := new(reportSet) // First, diagnose the go.mod file. modReports, modErr := mod.Diagnostics(ctx, snapshot) @@ -93,7 +172,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, alwaysA event.Error(ctx, "missing URI for module diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) continue } - addReport(id, true, diags) // treat go.mod diagnostics like analyses + reports.add(id, true, diags...) // treat go.mod diagnostics like analyses } // Diagnose all of the packages in the workspace. @@ -104,10 +183,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, alwaysA } // Some error messages can be displayed as diagnostics. if errList := (*source.ErrorList)(nil); errors.As(err, &errList) { - if r, err := errorsToDiagnostic(ctx, snapshot, *errList); err == nil { - for k, v := range r { - reports[k] = v - } + if err := errorsToDiagnostic(ctx, snapshot, *errList, reports); err == nil { return reports, nil } } @@ -172,7 +248,7 @@ If you believe this is a mistake, please file an issue: https://github.com/golan // Add all reports to the global map, checking for duplicates. for id, diags := range pkgReports { - addReport(id, withAnalysis, diags) + reports.add(id, withAnalysis, diags...) } // If gc optimization details are available, add them to the // diagnostic reports. @@ -182,7 +258,7 @@ If you believe this is a mistake, please file an issue: https://github.com/golan event.Error(ctx, "warning: gc details", err, tag.Snapshot.Of(snapshot.ID())) } for id, diags := range gcReports { - addReport(id, withAnalysis, diags) + reports.add(id, withAnalysis, diags...) } } }(pkg) @@ -196,7 +272,7 @@ If you believe this is a mistake, please file an issue: https://github.com/golan // meaning that we have already seen its package. var seen bool for _, withAnalysis := range []bool{true, false} { - _, ok := reports[idWithAnalysis{ + _, ok := reports.reports[idWithAnalysis{ id: o.VersionedFileIdentity(), withAnalysis: withAnalysis, }] @@ -209,7 +285,7 @@ If you believe this is a mistake, please file an issue: https://github.com/golan if diagnostic == nil { continue } - addReport(o.VersionedFileIdentity(), true, []*source.Diagnostic{diagnostic}) + reports.add(o.VersionedFileIdentity(), true, diagnostic) } } return reports, showMsg @@ -252,23 +328,7 @@ Otherwise, see the troubleshooting guidelines for help investigating (https://gi } } -// diagnosticKey creates a unique identifier for a given diagnostic, since we -// cannot use source.Diagnostics as map keys. This is used to de-duplicate -// diagnostics. -func diagnosticKey(d *source.Diagnostic) string { - var tags, related string - for _, t := range d.Tags { - tags += fmt.Sprintf("%s", t) - } - for _, r := range d.Related { - related += fmt.Sprintf("%s%s%s", r.URI, r.Message, r.Range) - } - key := fmt.Sprintf("%s%s%s%s%s%s", d.Message, d.Range, d.Severity, d.Source, tags, related) - return fmt.Sprintf("%x", sha256.Sum256([]byte(key))) -} - -func errorsToDiagnostic(ctx context.Context, snapshot source.Snapshot, errors []*source.Error) (map[idWithAnalysis]map[string]*source.Diagnostic, error) { - reports := make(map[idWithAnalysis]map[string]*source.Diagnostic) +func errorsToDiagnostic(ctx context.Context, snapshot source.Snapshot, errors []*source.Error, reports *reportSet) error { for _, e := range errors { diagnostic := &source.Diagnostic{ Range: e.Range, @@ -279,30 +339,23 @@ func errorsToDiagnostic(ctx context.Context, snapshot source.Snapshot, errors [] } fh, err := snapshot.GetFile(ctx, e.URI) if err != nil { - return nil, err + return err } - id := idWithAnalysis{ - id: fh.VersionedFileIdentity(), - withAnalysis: false, - } - if _, ok := reports[id]; !ok { - reports[id] = make(map[string]*source.Diagnostic) - } - reports[id][diagnosticKey(diagnostic)] = diagnostic + reports.add(fh.VersionedFileIdentity(), false, diagnostic) } - return reports, nil + return nil } -func (s *Server) publishReports(ctx context.Context, snapshot source.Snapshot, reports map[idWithAnalysis]map[string]*source.Diagnostic) { +func (s *Server) publishReports(ctx context.Context, snapshot source.Snapshot, reports *reportSet) { // Check for context cancellation before publishing diagnostics. - if ctx.Err() != nil { + if ctx.Err() != nil || reports == nil { return } s.deliveredMu.Lock() defer s.deliveredMu.Unlock() - for key, diagnosticsMap := range reports { + for key, diagnosticsMap := range reports.reports { // Don't deliver diagnostics if the context has already been canceled. if ctx.Err() != nil { break @@ -469,6 +522,7 @@ See https://github.com/golang/go/issues/39164 for more detail on this issue.`, if errors.Is(loadErr, source.PackagesLoadError) { // TODO(rstambler): Construct the diagnostics in internal/lsp/cache // so that we can avoid this here. + reports := new(reportSet) for _, uri := range snapshot.ModFiles() { fh, err := snapshot.GetFile(ctx, uri) if err != nil { @@ -478,9 +532,8 @@ See https://github.com/golang/go/issues/39164 for more detail on this issue.`, if err != nil { return false } - s.publishReports(ctx, snapshot, map[idWithAnalysis]map[string]*source.Diagnostic{ - {id: fh.VersionedFileIdentity()}: {diagnosticKey(diag): diag}, - }) + reports.add(fh.VersionedFileIdentity(), false, diag) + s.publishReports(ctx, snapshot, reports) return true } } diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go index a15a9472be..73ba92142b 100644 --- a/internal/lsp/fake/editor.go +++ b/internal/lsp/fake/editor.go @@ -204,6 +204,11 @@ func (e *Editor) configuration() map[string]interface{} { if !e.Config.WithoutExperimentalWorkspaceModule { config["experimentalWorkspaceModule"] = true } + + // TODO(rFindley): uncomment this if/when diagnostics delay is on by + // default... and probably change to the new settings name. + // config["experimentalDiagnosticsDelay"] = "10ms" + return config } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index dfc2222be4..daebcdfcd5 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -30,6 +30,7 @@ func NewServer(session source.Session, client protocol.Client) *Server { client: client, diagnosticsSema: make(chan struct{}, concurrentAnalyses), progress: newProgressTracker(client), + debouncer: newDebouncer(), } } @@ -94,6 +95,9 @@ type Server struct { diagnosticsSema chan struct{} progress *progressTracker + + // debouncer is used for debouncing diagnostics. + debouncer *debouncer } // sentDiagnostics is used to cache diagnostics that have been sent for a given file. diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index 7295d6423e..ae5b549cbc 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -2,4 +2,4 @@ package source -const GeneratedAPIJSON = "{\"Options\":{\"Debugging\":[{\"Name\":\"verboseOutput\",\"Type\":\"bool\",\"Doc\":\"verboseOutput enables additional debug logging.\\n\",\"EnumValues\":null,\"Default\":\"false\"},{\"Name\":\"completionBudget\",\"Type\":\"time.Duration\",\"Doc\":\"completionBudget is the soft latency goal for completion requests. Most\\nrequests finish in a couple milliseconds, but in some cases deep\\ncompletions can take much longer. As we use up our budget we\\ndynamically reduce the search scope to ensure we return timely\\nresults. Zero means unlimited.\\n\",\"EnumValues\":null,\"Default\":\"100000000\"}],\"Experimental\":[{\"Name\":\"analyses\",\"Type\":\"map[string]bool\",\"Doc\":\"analyses specify analyses that the user would like to enable or disable.\\nA map of the names of analysis passes that should be enabled/disabled.\\nA full list of analyzers that gopls uses can be found [here](analyzers.md)\\n\\nExample Usage:\\n```json5\\n...\\n\\\"analyses\\\": {\\n \\\"unreachable\\\": false, // Disable the unreachable analyzer.\\n \\\"unusedparams\\\": true // Enable the unusedparams analyzer.\\n}\\n...\\n```\\n\",\"EnumValues\":null,\"Default\":\"{}\"},{\"Name\":\"codelens\",\"Type\":\"map[string]bool\",\"Doc\":\"codelens overrides the enabled/disabled state of code lenses. See the \\\"Code Lenses\\\"\\nsection of settings.md for the list of supported lenses.\\n\\nExample Usage:\\n```json5\\n\\\"gopls\\\": {\\n...\\n \\\"codelens\\\": {\\n \\\"generate\\\": false, // Don't show the `go generate` lens.\\n \\\"gc_details\\\": true // Show a code lens toggling the display of gc's choices.\\n }\\n...\\n}\\n```\\n\",\"EnumValues\":null,\"Default\":\"{\\\"gc_details\\\":false,\\\"generate\\\":true,\\\"regenerate_cgo\\\":true,\\\"tidy\\\":true,\\\"upgrade_dependency\\\":true,\\\"vendor\\\":true}\"},{\"Name\":\"completionDocumentation\",\"Type\":\"bool\",\"Doc\":\"completionDocumentation enables documentation with completion results.\\n\",\"EnumValues\":null,\"Default\":\"true\"},{\"Name\":\"completeUnimported\",\"Type\":\"bool\",\"Doc\":\"completeUnimported enables completion for packages that you do not currently import.\\n\",\"EnumValues\":null,\"Default\":\"true\"},{\"Name\":\"deepCompletion\",\"Type\":\"bool\",\"Doc\":\"deepCompletion enables the ability to return completions from deep inside relevant entities, rather than just the locally accessible ones.\\n\\nConsider this example:\\n\\n```go\\npackage main\\n\\nimport \\\"fmt\\\"\\n\\ntype wrapString struct {\\n str string\\n}\\n\\nfunc main() {\\n x := wrapString{\\\"hello world\\\"}\\n fmt.Printf(\\u003c\\u003e)\\n}\\n```\\n\\nAt the location of the `\\u003c\\u003e` in this program, deep completion would suggest the result `x.str`.\\n\",\"EnumValues\":null,\"Default\":\"true\"},{\"Name\":\"matcher\",\"Type\":\"enum\",\"Doc\":\"matcher sets the algorithm that is used when calculating completion candidates.\\n\",\"EnumValues\":[\"\\\"CaseInsensitive\\\"\",\"\\\"CaseSensitive\\\"\",\"\\\"Fuzzy\\\"\"],\"Default\":\"\\\"Fuzzy\\\"\"},{\"Name\":\"annotations\",\"Type\":\"map[string]bool\",\"Doc\":\"annotations suppress various kinds of optimization diagnostics\\nthat would be reported by the gc_details command.\\n * noNilcheck suppresses display of nilchecks.\\n * noEscape suppresses escape choices.\\n * noInline suppresses inlining choices.\\n * noBounds suppresses bounds checking diagnostics.\\n\",\"EnumValues\":null,\"Default\":\"{}\"},{\"Name\":\"staticcheck\",\"Type\":\"bool\",\"Doc\":\"staticcheck enables additional analyses from staticcheck.io.\\n\",\"EnumValues\":null,\"Default\":\"false\"},{\"Name\":\"symbolMatcher\",\"Type\":\"enum\",\"Doc\":\"symbolMatcher sets the algorithm that is used when finding workspace symbols.\\n\",\"EnumValues\":[\"\\\"CaseInsensitive\\\"\",\"\\\"CaseSensitive\\\"\",\"\\\"Fuzzy\\\"\"],\"Default\":\"\\\"Fuzzy\\\"\"},{\"Name\":\"symbolStyle\",\"Type\":\"enum\",\"Doc\":\"symbolStyle specifies what style of symbols to return in symbol requests.\\n\",\"EnumValues\":[\"\\\"Dynamic\\\"\",\"\\\"Full\\\"\",\"\\\"Package\\\"\"],\"Default\":\"\\\"Package\\\"\"},{\"Name\":\"linksInHover\",\"Type\":\"bool\",\"Doc\":\"linksInHover toggles the presence of links to documentation in hover.\\n\",\"EnumValues\":null,\"Default\":\"true\"},{\"Name\":\"tempModfile\",\"Type\":\"bool\",\"Doc\":\"tempModfile controls the use of the -modfile flag in Go 1.14.\\n\",\"EnumValues\":null,\"Default\":\"true\"},{\"Name\":\"importShortcut\",\"Type\":\"enum\",\"Doc\":\"importShortcut specifies whether import statements should link to\\ndocumentation or go to definitions.\\n\",\"EnumValues\":[\"\\\"Both\\\"\",\"\\\"Definition\\\"\",\"\\\"Link\\\"\"],\"Default\":\"\\\"Both\\\"\"},{\"Name\":\"verboseWorkDoneProgress\",\"Type\":\"bool\",\"Doc\":\"verboseWorkDoneProgress controls whether the LSP server should send\\nprogress reports for all work done outside the scope of an RPC.\\n\",\"EnumValues\":null,\"Default\":\"false\"},{\"Name\":\"semanticTokens\",\"Type\":\"bool\",\"Doc\":\"semanticTokens controls whether the LSP server will send\\nsemantic tokens to the client.\\n\",\"EnumValues\":null,\"Default\":\"false\"},{\"Name\":\"expandWorkspaceToModule\",\"Type\":\"bool\",\"Doc\":\"expandWorkspaceToModule instructs `gopls` to expand the scope of the workspace to include the\\nmodules containing the workspace folders. Set this to false to avoid loading\\nyour entire module. This is particularly useful for those working in a monorepo.\\n\",\"EnumValues\":null,\"Default\":\"true\"},{\"Name\":\"experimentalWorkspaceModule\",\"Type\":\"bool\",\"Doc\":\"experimentalWorkspaceModule opts a user into the experimental support\\nfor multi-module workspaces.\\n\",\"EnumValues\":null,\"Default\":\"false\"},{\"Name\":\"literalCompletions\",\"Type\":\"bool\",\"Doc\":\"literalCompletions controls whether literal candidates such as\\n\\\"\\u0026someStruct{}\\\" are offered. Tests disable this flag to simplify\\ntheir expected values.\\n\",\"EnumValues\":null,\"Default\":\"true\"}],\"User\":[{\"Name\":\"buildFlags\",\"Type\":\"[]string\",\"Doc\":\"buildFlags is the set of flags passed on to the build system when invoked.\\nIt is applied to queries like `go list`, which is used when discovering files.\\nThe most common use is to set `-tags`.\\n\",\"EnumValues\":null,\"Default\":\"[]\"},{\"Name\":\"env\",\"Type\":\"[]string\",\"Doc\":\"env adds environment variables to external commands run by `gopls`, most notably `go list`.\\n\",\"EnumValues\":null,\"Default\":\"[]\"},{\"Name\":\"hoverKind\",\"Type\":\"enum\",\"Doc\":\"hoverKind controls the information that appears in the hover text.\\nSingleLine and Structured are intended for use only by authors of editor plugins.\\n\",\"EnumValues\":[\"\\\"FullDocumentation\\\"\",\"\\\"NoDocumentation\\\"\",\"\\\"SingleLine\\\"\",\"\\\"Structured\\\"\",\"\\\"SynopsisDocumentation\\\"\"],\"Default\":\"\\\"FullDocumentation\\\"\"},{\"Name\":\"usePlaceholders\",\"Type\":\"bool\",\"Doc\":\"placeholders enables placeholders for function parameters or struct fields in completion responses.\\n\",\"EnumValues\":null,\"Default\":\"false\"},{\"Name\":\"linkTarget\",\"Type\":\"string\",\"Doc\":\"linkTarget controls where documentation links go.\\nIt might be one of:\\n\\n* `\\\"godoc.org\\\"`\\n* `\\\"pkg.go.dev\\\"`\\n\\nIf company chooses to use its own `godoc.org`, its address can be used as well.\\n\",\"EnumValues\":null,\"Default\":\"\\\"pkg.go.dev\\\"\"},{\"Name\":\"local\",\"Type\":\"string\",\"Doc\":\"local is the equivalent of the `goimports -local` flag, which puts imports beginning with this string after 3rd-party packages.\\nIt should be the prefix of the import path whose imports should be grouped separately.\\n\",\"EnumValues\":null,\"Default\":\"\\\"\\\"\"},{\"Name\":\"gofumpt\",\"Type\":\"bool\",\"Doc\":\"gofumpt indicates if we should run gofumpt formatting.\\n\",\"EnumValues\":null,\"Default\":\"false\"}]},\"Commands\":[{\"Command\":\"generate\",\"Title\":\"Run go generate\",\"Doc\":\"generate runs `go generate` for a given directory.\\n\"},{\"Command\":\"fill_struct\",\"Title\":\"fill_struct\",\"Doc\":\"fill_struct is a gopls command to fill a struct with default\\nvalues.\\n\"},{\"Command\":\"regenerate_cgo\",\"Title\":\"Regenerate cgo\",\"Doc\":\"regenerate_cgo regenerates cgo definitions.\\n\"},{\"Command\":\"test\",\"Title\":\"Run test(s)\",\"Doc\":\"test runs `go test` for a specific test function.\\n\"},{\"Command\":\"tidy\",\"Title\":\"Run go mod tidy\",\"Doc\":\"tidy runs `go mod tidy` for a module.\\n\"},{\"Command\":\"undeclared_name\",\"Title\":\"undeclared_name\",\"Doc\":\"undeclared_name adds a variable declaration for an undeclared\\nname.\\n\"},{\"Command\":\"upgrade_dependency\",\"Title\":\"Upgrade dependency\",\"Doc\":\"upgrade_dependency upgrades a dependency.\\n\"},{\"Command\":\"vendor\",\"Title\":\"Run go mod vendor\",\"Doc\":\"vendor runs `go mod vendor` for a module.\\n\"},{\"Command\":\"extract_variable\",\"Title\":\"Extract to variable\",\"Doc\":\"extract_variable extracts an expression to a variable.\\n\"},{\"Command\":\"extract_function\",\"Title\":\"Extract to function\",\"Doc\":\"extract_function extracts statements to a function.\\n\"},{\"Command\":\"gc_details\",\"Title\":\"Toggle gc_details\",\"Doc\":\"gc_details controls calculation of gc annotations.\\n\"},{\"Command\":\"generate_gopls_mod\",\"Title\":\"Generate gopls.mod\",\"Doc\":\"generate_gopls_mod (re)generates the gopls.mod file.\\n\"}],\"Lenses\":[{\"Lens\":\"generate\",\"Title\":\"Run go generate\",\"Doc\":\"generate runs `go generate` for a given directory.\\n\"},{\"Lens\":\"regenerate_cgo\",\"Title\":\"Regenerate cgo\",\"Doc\":\"regenerate_cgo regenerates cgo definitions.\\n\"},{\"Lens\":\"test\",\"Title\":\"Run test(s)\",\"Doc\":\"test runs `go test` for a specific test function.\\n\"},{\"Lens\":\"tidy\",\"Title\":\"Run go mod tidy\",\"Doc\":\"tidy runs `go mod tidy` for a module.\\n\"},{\"Lens\":\"upgrade_dependency\",\"Title\":\"Upgrade dependency\",\"Doc\":\"upgrade_dependency upgrades a dependency.\\n\"},{\"Lens\":\"vendor\",\"Title\":\"Run go mod vendor\",\"Doc\":\"vendor runs `go mod vendor` for a module.\\n\"},{\"Lens\":\"gc_details\",\"Title\":\"Toggle gc_details\",\"Doc\":\"gc_details controls calculation of gc annotations.\\n\"}]}" +const GeneratedAPIJSON = "{\"Options\":{\"Debugging\":[{\"Name\":\"verboseOutput\",\"Type\":\"bool\",\"Doc\":\"verboseOutput enables additional debug logging.\\n\",\"EnumValues\":null,\"Default\":\"false\"},{\"Name\":\"completionBudget\",\"Type\":\"time.Duration\",\"Doc\":\"completionBudget is the soft latency goal for completion requests. Most\\nrequests finish in a couple milliseconds, but in some cases deep\\ncompletions can take much longer. As we use up our budget we\\ndynamically reduce the search scope to ensure we return timely\\nresults. Zero means unlimited.\\n\",\"EnumValues\":null,\"Default\":\"100000000\"}],\"Experimental\":[{\"Name\":\"analyses\",\"Type\":\"map[string]bool\",\"Doc\":\"analyses specify analyses that the user would like to enable or disable.\\nA map of the names of analysis passes that should be enabled/disabled.\\nA full list of analyzers that gopls uses can be found [here](analyzers.md)\\n\\nExample Usage:\\n```json5\\n...\\n\\\"analyses\\\": {\\n \\\"unreachable\\\": false, // Disable the unreachable analyzer.\\n \\\"unusedparams\\\": true // Enable the unusedparams analyzer.\\n}\\n...\\n```\\n\",\"EnumValues\":null,\"Default\":\"{}\"},{\"Name\":\"codelens\",\"Type\":\"map[string]bool\",\"Doc\":\"codelens overrides the enabled/disabled state of code lenses. See the \\\"Code Lenses\\\"\\nsection of settings.md for the list of supported lenses.\\n\\nExample Usage:\\n```json5\\n\\\"gopls\\\": {\\n...\\n \\\"codelens\\\": {\\n \\\"generate\\\": false, // Don't show the `go generate` lens.\\n \\\"gc_details\\\": true // Show a code lens toggling the display of gc's choices.\\n }\\n...\\n}\\n```\\n\",\"EnumValues\":null,\"Default\":\"{\\\"gc_details\\\":false,\\\"generate\\\":true,\\\"regenerate_cgo\\\":true,\\\"tidy\\\":true,\\\"upgrade_dependency\\\":true,\\\"vendor\\\":true}\"},{\"Name\":\"completionDocumentation\",\"Type\":\"bool\",\"Doc\":\"completionDocumentation enables documentation with completion results.\\n\",\"EnumValues\":null,\"Default\":\"true\"},{\"Name\":\"completeUnimported\",\"Type\":\"bool\",\"Doc\":\"completeUnimported enables completion for packages that you do not currently import.\\n\",\"EnumValues\":null,\"Default\":\"true\"},{\"Name\":\"deepCompletion\",\"Type\":\"bool\",\"Doc\":\"deepCompletion enables the ability to return completions from deep inside relevant entities, rather than just the locally accessible ones.\\n\\nConsider this example:\\n\\n```go\\npackage main\\n\\nimport \\\"fmt\\\"\\n\\ntype wrapString struct {\\n str string\\n}\\n\\nfunc main() {\\n x := wrapString{\\\"hello world\\\"}\\n fmt.Printf(\\u003c\\u003e)\\n}\\n```\\n\\nAt the location of the `\\u003c\\u003e` in this program, deep completion would suggest the result `x.str`.\\n\",\"EnumValues\":null,\"Default\":\"true\"},{\"Name\":\"matcher\",\"Type\":\"enum\",\"Doc\":\"matcher sets the algorithm that is used when calculating completion candidates.\\n\",\"EnumValues\":[\"\\\"CaseInsensitive\\\"\",\"\\\"CaseSensitive\\\"\",\"\\\"Fuzzy\\\"\"],\"Default\":\"\\\"Fuzzy\\\"\"},{\"Name\":\"annotations\",\"Type\":\"map[string]bool\",\"Doc\":\"annotations suppress various kinds of optimization diagnostics\\nthat would be reported by the gc_details command.\\n * noNilcheck suppresses display of nilchecks.\\n * noEscape suppresses escape choices.\\n * noInline suppresses inlining choices.\\n * noBounds suppresses bounds checking diagnostics.\\n\",\"EnumValues\":null,\"Default\":\"{}\"},{\"Name\":\"staticcheck\",\"Type\":\"bool\",\"Doc\":\"staticcheck enables additional analyses from staticcheck.io.\\n\",\"EnumValues\":null,\"Default\":\"false\"},{\"Name\":\"symbolMatcher\",\"Type\":\"enum\",\"Doc\":\"symbolMatcher sets the algorithm that is used when finding workspace symbols.\\n\",\"EnumValues\":[\"\\\"CaseInsensitive\\\"\",\"\\\"CaseSensitive\\\"\",\"\\\"Fuzzy\\\"\"],\"Default\":\"\\\"Fuzzy\\\"\"},{\"Name\":\"symbolStyle\",\"Type\":\"enum\",\"Doc\":\"symbolStyle specifies what style of symbols to return in symbol requests.\\n\",\"EnumValues\":[\"\\\"Dynamic\\\"\",\"\\\"Full\\\"\",\"\\\"Package\\\"\"],\"Default\":\"\\\"Package\\\"\"},{\"Name\":\"linksInHover\",\"Type\":\"bool\",\"Doc\":\"linksInHover toggles the presence of links to documentation in hover.\\n\",\"EnumValues\":null,\"Default\":\"true\"},{\"Name\":\"tempModfile\",\"Type\":\"bool\",\"Doc\":\"tempModfile controls the use of the -modfile flag in Go 1.14.\\n\",\"EnumValues\":null,\"Default\":\"true\"},{\"Name\":\"importShortcut\",\"Type\":\"enum\",\"Doc\":\"importShortcut specifies whether import statements should link to\\ndocumentation or go to definitions.\\n\",\"EnumValues\":[\"\\\"Both\\\"\",\"\\\"Definition\\\"\",\"\\\"Link\\\"\"],\"Default\":\"\\\"Both\\\"\"},{\"Name\":\"verboseWorkDoneProgress\",\"Type\":\"bool\",\"Doc\":\"verboseWorkDoneProgress controls whether the LSP server should send\\nprogress reports for all work done outside the scope of an RPC.\\n\",\"EnumValues\":null,\"Default\":\"false\"},{\"Name\":\"semanticTokens\",\"Type\":\"bool\",\"Doc\":\"semanticTokens controls whether the LSP server will send\\nsemantic tokens to the client.\\n\",\"EnumValues\":null,\"Default\":\"false\"},{\"Name\":\"expandWorkspaceToModule\",\"Type\":\"bool\",\"Doc\":\"expandWorkspaceToModule instructs `gopls` to expand the scope of the workspace to include the\\nmodules containing the workspace folders. Set this to false to avoid loading\\nyour entire module. This is particularly useful for those working in a monorepo.\\n\",\"EnumValues\":null,\"Default\":\"true\"},{\"Name\":\"experimentalWorkspaceModule\",\"Type\":\"bool\",\"Doc\":\"experimentalWorkspaceModule opts a user into the experimental support\\nfor multi-module workspaces.\\n\",\"EnumValues\":null,\"Default\":\"false\"},{\"Name\":\"literalCompletions\",\"Type\":\"bool\",\"Doc\":\"literalCompletions controls whether literal candidates such as\\n\\\"\\u0026someStruct{}\\\" are offered. Tests disable this flag to simplify\\ntheir expected values.\\n\",\"EnumValues\":null,\"Default\":\"true\"},{\"Name\":\"experimentalDiagnosticsDelay\",\"Type\":\"time.Duration\",\"Doc\":\"experimentalDiagnosticsDelay controls the amount of time that gopls waits\\nafter the most recent file modification before computing deep diagnostics.\\nSimple diagnostics (parsing and type-checking) are always run immediately\\non recently modified packages.\\n\\nThis option must be set to a valid duration string, for example `\\\"250ms\\\"`.\\n\",\"EnumValues\":null,\"Default\":\"0\"}],\"User\":[{\"Name\":\"buildFlags\",\"Type\":\"[]string\",\"Doc\":\"buildFlags is the set of flags passed on to the build system when invoked.\\nIt is applied to queries like `go list`, which is used when discovering files.\\nThe most common use is to set `-tags`.\\n\",\"EnumValues\":null,\"Default\":\"[]\"},{\"Name\":\"env\",\"Type\":\"[]string\",\"Doc\":\"env adds environment variables to external commands run by `gopls`, most notably `go list`.\\n\",\"EnumValues\":null,\"Default\":\"[]\"},{\"Name\":\"hoverKind\",\"Type\":\"enum\",\"Doc\":\"hoverKind controls the information that appears in the hover text.\\nSingleLine and Structured are intended for use only by authors of editor plugins.\\n\",\"EnumValues\":[\"\\\"FullDocumentation\\\"\",\"\\\"NoDocumentation\\\"\",\"\\\"SingleLine\\\"\",\"\\\"Structured\\\"\",\"\\\"SynopsisDocumentation\\\"\"],\"Default\":\"\\\"FullDocumentation\\\"\"},{\"Name\":\"usePlaceholders\",\"Type\":\"bool\",\"Doc\":\"placeholders enables placeholders for function parameters or struct fields in completion responses.\\n\",\"EnumValues\":null,\"Default\":\"false\"},{\"Name\":\"linkTarget\",\"Type\":\"string\",\"Doc\":\"linkTarget controls where documentation links go.\\nIt might be one of:\\n\\n* `\\\"godoc.org\\\"`\\n* `\\\"pkg.go.dev\\\"`\\n\\nIf company chooses to use its own `godoc.org`, its address can be used as well.\\n\",\"EnumValues\":null,\"Default\":\"\\\"pkg.go.dev\\\"\"},{\"Name\":\"local\",\"Type\":\"string\",\"Doc\":\"local is the equivalent of the `goimports -local` flag, which puts imports beginning with this string after 3rd-party packages.\\nIt should be the prefix of the import path whose imports should be grouped separately.\\n\",\"EnumValues\":null,\"Default\":\"\\\"\\\"\"},{\"Name\":\"gofumpt\",\"Type\":\"bool\",\"Doc\":\"gofumpt indicates if we should run gofumpt formatting.\\n\",\"EnumValues\":null,\"Default\":\"false\"}]},\"Commands\":[{\"Command\":\"generate\",\"Title\":\"Run go generate\",\"Doc\":\"generate runs `go generate` for a given directory.\\n\"},{\"Command\":\"fill_struct\",\"Title\":\"fill_struct\",\"Doc\":\"fill_struct is a gopls command to fill a struct with default\\nvalues.\\n\"},{\"Command\":\"regenerate_cgo\",\"Title\":\"Regenerate cgo\",\"Doc\":\"regenerate_cgo regenerates cgo definitions.\\n\"},{\"Command\":\"test\",\"Title\":\"Run test(s)\",\"Doc\":\"test runs `go test` for a specific test function.\\n\"},{\"Command\":\"tidy\",\"Title\":\"Run go mod tidy\",\"Doc\":\"tidy runs `go mod tidy` for a module.\\n\"},{\"Command\":\"undeclared_name\",\"Title\":\"undeclared_name\",\"Doc\":\"undeclared_name adds a variable declaration for an undeclared\\nname.\\n\"},{\"Command\":\"upgrade_dependency\",\"Title\":\"Upgrade dependency\",\"Doc\":\"upgrade_dependency upgrades a dependency.\\n\"},{\"Command\":\"vendor\",\"Title\":\"Run go mod vendor\",\"Doc\":\"vendor runs `go mod vendor` for a module.\\n\"},{\"Command\":\"extract_variable\",\"Title\":\"Extract to variable\",\"Doc\":\"extract_variable extracts an expression to a variable.\\n\"},{\"Command\":\"extract_function\",\"Title\":\"Extract to function\",\"Doc\":\"extract_function extracts statements to a function.\\n\"},{\"Command\":\"gc_details\",\"Title\":\"Toggle gc_details\",\"Doc\":\"gc_details controls calculation of gc annotations.\\n\"},{\"Command\":\"generate_gopls_mod\",\"Title\":\"Generate gopls.mod\",\"Doc\":\"generate_gopls_mod (re)generates the gopls.mod file.\\n\"}],\"Lenses\":[{\"Lens\":\"generate\",\"Title\":\"Run go generate\",\"Doc\":\"generate runs `go generate` for a given directory.\\n\"},{\"Lens\":\"regenerate_cgo\",\"Title\":\"Regenerate cgo\",\"Doc\":\"regenerate_cgo regenerates cgo definitions.\\n\"},{\"Lens\":\"test\",\"Title\":\"Run test(s)\",\"Doc\":\"test runs `go test` for a specific test function.\\n\"},{\"Lens\":\"tidy\",\"Title\":\"Run go mod tidy\",\"Doc\":\"tidy runs `go mod tidy` for a module.\\n\"},{\"Lens\":\"upgrade_dependency\",\"Title\":\"Upgrade dependency\",\"Doc\":\"upgrade_dependency upgrades a dependency.\\n\"},{\"Lens\":\"vendor\",\"Title\":\"Run go mod vendor\",\"Doc\":\"vendor runs `go mod vendor` for a module.\\n\"},{\"Lens\":\"gc_details\",\"Title\":\"Toggle gc_details\",\"Doc\":\"gc_details controls calculation of gc annotations.\\n\"}]}" diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index c1084afb53..7b913d9749 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -330,6 +330,14 @@ type ExperimentalOptions struct { // "&someStruct{}" are offered. Tests disable this flag to simplify // their expected values. LiteralCompletions bool + + // ExperimentalDiagnosticsDelay controls the amount of time that gopls waits + // after the most recent file modification before computing deep diagnostics. + // Simple diagnostics (parsing and type-checking) are always run immediately + // on recently modified packages. + // + // This option must be set to a valid duration string, for example `"250ms"`. + ExperimentalDiagnosticsDelay time.Duration } // DebuggingOptions should not affect the logical execution of Gopls, but may @@ -548,15 +556,7 @@ func (o *Options) set(name string, value interface{}) OptionResult { case "completeUnimported": result.setBool(&o.CompleteUnimported) case "completionBudget": - if v, ok := result.asString(); ok { - d, err := time.ParseDuration(v) - if err != nil { - result.errorf("failed to parse duration %q: %v", v, err) - break - } - o.CompletionBudget = d - } - + result.setDuration(&o.CompletionBudget) case "matcher": matcher, ok := result.asString() if !ok { @@ -693,6 +693,9 @@ func (o *Options) set(name string, value interface{}) OptionResult { case "experimentalWorkspaceModule": result.setBool(&o.ExperimentalWorkspaceModule) + case "experimentalDiagnosticsDelay": + result.setDuration(&o.ExperimentalDiagnosticsDelay) + // Replaced settings. case "experimentalDisabledAnalyses": result.State = OptionDeprecated @@ -760,6 +763,17 @@ func (r *OptionResult) setBool(b *bool) { } } +func (r *OptionResult) setDuration(d *time.Duration) { + if v, ok := r.asString(); ok { + parsed, err := time.ParseDuration(v) + if err != nil { + r.errorf("failed to parse duration %q: %v", v, err) + return + } + *d = parsed + } +} + func (r *OptionResult) setBoolMap(bm *map[string]bool) { all, ok := r.Value.(map[string]interface{}) if !ok { diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go index e1a80eb792..a806477097 100644 --- a/internal/lsp/text_synchronization.go +++ b/internal/lsp/text_synchronization.go @@ -191,6 +191,7 @@ func (s *Server) didModifyFiles(ctx context.Context, modifications []source.File return err } + // Clear out diagnostics for deleted files. for _, uri := range deletions { if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ URI: protocol.URIFromSpanURI(uri), @@ -254,7 +255,7 @@ func (s *Server) didModifyFiles(ctx context.Context, modifications []source.File diagnosticWG.Add(1) go func(snapshot source.Snapshot, uris []span.URI) { defer diagnosticWG.Done() - s.diagnoseSnapshot(snapshot) + s.diagnoseSnapshot(snapshot, uris) }(snapshot, uris) }