internal/lsp: fix flickering analysis diagnostics
Also, change withAnalysis to includesAnalysis to make the name a bit
more accurate.
Fixes golang/go#42363
Change-Id: If50f7d36b953e6627ed9ba049357a2b86dc3f676
Reviewed-on: https://go-review.googlesource.com/c/tools/+/267817
Trust: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 92f2e46..f878108 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -26,8 +26,8 @@
// idWithAnalysis is used to track if the diagnostics for a given file were
// computed with analyses.
type idWithAnalysis struct {
- id source.VersionedFileIdentity
- withAnalysis bool
+ id source.VersionedFileIdentity
+ includeAnalysis bool
}
// A reportSet collects diagnostics for publication, sorting them by file and
@@ -38,15 +38,15 @@
reports map[idWithAnalysis]map[string]*source.Diagnostic
}
-func (s *reportSet) add(id source.VersionedFileIdentity, withAnalysis bool, diags ...*source.Diagnostic) {
+func (s *reportSet) add(id source.VersionedFileIdentity, includeAnalysis 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,
+ id: id,
+ includeAnalysis: includeAnalysis,
}
if _, ok := s.reports[key]; !ok {
s.reports[key] = map[string]*source.Diagnostic{}
@@ -79,7 +79,7 @@
// If a view has been created or the configuration changed, warn the user.
s.client.ShowMessage(ctx, shows)
}
- s.publishReports(ctx, snapshot, reports)
+ s.publishReports(ctx, snapshot, reports, false)
}
func (s *Server) diagnoseSnapshot(snapshot source.Snapshot, changedURIs []span.URI) {
@@ -99,17 +99,17 @@
event.Error(ctx, "diagnosing changed files", err)
}
}
- s.publishReports(ctx, snapshot, reports)
+ s.publishReports(ctx, snapshot, reports, true)
s.debouncer.debounce(snapshot.View().Name(), snapshot.ID(), delay, func() {
reports, _ := s.diagnose(ctx, snapshot, false)
- s.publishReports(ctx, snapshot, reports)
+ s.publishReports(ctx, snapshot, reports, false)
})
return
}
// Ignore possible workspace configuration warnings in the normal flow.
reports, _ := s.diagnose(ctx, snapshot, false)
- s.publishReports(ctx, snapshot, reports)
+ s.publishReports(ctx, snapshot, reports, false)
}
func (s *Server) diagnoseChangedFiles(ctx context.Context, snapshot source.Snapshot, uris []span.URI) (*reportSet, error) {
@@ -215,11 +215,11 @@
go func(pkg source.Package) {
defer wg.Done()
- withAnalysis := alwaysAnalyze // only run analyses for packages with open files
- var gcDetailsDir span.URI // find the package's optimization details, if available
+ includeAnalysis := alwaysAnalyze // only run analyses for packages with open files
+ var gcDetailsDir span.URI // find the package's optimization details, if available
for _, pgf := range pkg.CompiledGoFiles() {
if snapshot.IsOpen(pgf.URI) {
- withAnalysis = true
+ includeAnalysis = true
}
if gcDetailsDir == "" {
dirURI := span.URIFromPath(filepath.Dir(pgf.URI.Filename()))
@@ -232,7 +232,7 @@
}
}
- pkgReports, warn, err := source.Diagnostics(ctx, snapshot, pkg, withAnalysis)
+ pkgReports, warn, err := source.Diagnostics(ctx, snapshot, pkg, includeAnalysis)
// Check if might want to warn the user about their build configuration.
// Our caller decides whether to send the message.
@@ -251,7 +251,7 @@
// Add all reports to the global map, checking for duplicates.
for id, diags := range pkgReports {
- reports.add(id, withAnalysis, diags...)
+ reports.add(id, includeAnalysis, diags...)
}
// If gc optimization details are available, add them to the
// diagnostic reports.
@@ -261,7 +261,7 @@
event.Error(ctx, "warning: gc details", err, tag.Snapshot.Of(snapshot.ID()))
}
for id, diags := range gcReports {
- reports.add(id, withAnalysis, diags...)
+ reports.add(id, includeAnalysis, diags...)
}
}
}(pkg)
@@ -274,10 +274,10 @@
// Check if we already have diagnostic reports for the given file,
// meaning that we have already seen its package.
var seen bool
- for _, withAnalysis := range []bool{true, false} {
+ for _, includeAnalysis := range []bool{true, false} {
_, ok := reports.reports[idWithAnalysis{
- id: o.VersionedFileIdentity(),
- withAnalysis: withAnalysis,
+ id: o.VersionedFileIdentity(),
+ includeAnalysis: includeAnalysis,
}]
seen = seen || ok
}
@@ -349,7 +349,7 @@
return nil
}
-func (s *Server) publishReports(ctx context.Context, snapshot source.Snapshot, reports *reportSet) {
+func (s *Server) publishReports(ctx context.Context, snapshot source.Snapshot, reports *reportSet, isFirstPass bool) {
// Check for context cancellation before publishing diagnostics.
if ctx.Err() != nil || reports == nil {
return
@@ -370,10 +370,10 @@
}
source.SortDiagnostics(diagnostics)
toSend := sentDiagnostics{
- id: key.id,
- sorted: diagnostics,
- withAnalysis: key.withAnalysis,
- snapshotID: snapshot.ID(),
+ id: key.id,
+ sorted: diagnostics,
+ includeAnalysis: key.includeAnalysis,
+ snapshotID: snapshot.ID(),
}
// We use the zero values if this is an unknown file.
@@ -382,10 +382,11 @@
// Snapshot IDs are always increasing, so we use them instead of file
// versions to create the correct order for diagnostics.
- // If we've already delivered diagnostics for a future snapshot for this file,
- // do not deliver them.
+ // If we've already delivered diagnostics for a future snapshot for
+ // this file, do not deliver them.
if delivered.snapshotID > toSend.snapshotID {
- // Do not update the delivered map since it already contains newer diagnostics.
+ // Do not update the delivered map since it already contains newer
+ // diagnostics.
continue
}
@@ -399,10 +400,18 @@
// If we've already delivered diagnostics for this file, at this
// snapshot, with analyses, do not send diagnostics without analyses.
if delivered.snapshotID == toSend.snapshotID && delivered.id == toSend.id &&
- delivered.withAnalysis && !toSend.withAnalysis {
+ delivered.includeAnalysis && !toSend.includeAnalysis {
// Do not update the delivered map since it already contains better diagnostics.
continue
}
+
+ // If we've previously delivered non-empty diagnostics and this is a
+ // first diagnostic pass, wait for a subsequent pass to complete before
+ // sending empty diagnostics to avoid flickering diagnostics.
+ if isFirstPass && delivered.includeAnalysis && !toSend.includeAnalysis && len(toSend.sorted) == 0 {
+ continue
+ }
+
if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
Diagnostics: toProtocolDiagnostics(diagnostics),
URI: protocol.URIFromSpanURI(key.id.URI),
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index aa73f6c..0303491 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -1159,7 +1159,7 @@
// Always run diagnostics with analysis.
reports, _ := r.server.diagnose(r.ctx, snapshot, true)
- r.server.publishReports(r.ctx, snapshot, reports)
+ r.server.publishReports(r.ctx, snapshot, reports, false)
for uri, sent := range r.server.delivered {
var diagnostics []*source.Diagnostic
for _, d := range sent.sorted {
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 058ffbc..c2f5004 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -107,10 +107,10 @@
// sentDiagnostics is used to cache diagnostics that have been sent for a given file.
type sentDiagnostics struct {
- id source.VersionedFileIdentity
- sorted []*source.Diagnostic
- withAnalysis bool
- snapshotID uint64
+ id source.VersionedFileIdentity
+ sorted []*source.Diagnostic
+ includeAnalysis bool
+ snapshotID uint64
}
func (s *Server) workDoneProgressCancel(ctx context.Context, params *protocol.WorkDoneProgressCancelParams) error {