Skip to content

Commit

Permalink
Streaming workspace diagnostics
Browse files Browse the repository at this point in the history
  • Loading branch information
kralicky committed Jul 15, 2023
1 parent 70a0e94 commit f0c9a04
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 134 deletions.
73 changes: 47 additions & 26 deletions pkg/lsp/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,9 @@ func (c *Cache) ComputeDiagnosticReports(uri span.URI, prevResultId string) ([]p
return nil, protocol.DiagnosticUnchanged, resultId, nil
}
protocolReports := c.toProtocolDiagnostics(rawReports)
if protocolReports == nil {
protocolReports = []protocol.Diagnostic{}
}

return protocolReports, protocol.DiagnosticFull, resultId, nil
}
Expand All @@ -574,36 +577,54 @@ func (c *Cache) toProtocolDiagnostics(rawReports []*ProtoDiagnostic) []protocol.

type workspaceDiagnosticCallbackFunc = func(uri span.URI, reports []protocol.Diagnostic, kind protocol.DocumentDiagnosticReportKind, resultId string)

func (c *Cache) ComputeWorkspaceDiagnosticReports(ctx context.Context, previousResultIds []protocol.PreviousResultID, callback workspaceDiagnosticCallbackFunc) bool {
c.resultsMu.RLock()
defer c.resultsMu.RUnlock()
var prevResultMapByPath map[string]string
return c.diagHandler.MaybeRange(func() {
prevResultMapByPath = make(map[string]string, len(previousResultIds))
for _, prevResult := range previousResultIds {
if p, err := c.resolver.URIToPath(prevResult.URI.SpanURI()); err == nil {
prevResultMapByPath[p] = prevResult.Value
}
}
}, func(s string, dl *DiagnosticList) bool {
var maybePrevResultId []string
prevResultId, ok := prevResultMapByPath[s]
if ok {
maybePrevResultId = append(maybePrevResultId, prevResultId)
}
uri, err := c.resolver.PathToURI(s)
func (c *Cache) StreamWorkspaceDiagnostics(ctx context.Context, ch chan<- protocol.WorkspaceDiagnosticReportPartialResult) {
currentDiagnostics := make(map[span.URI][]protocol.Diagnostic)
diagnosticVersions := make(map[span.URI]int32)
c.diagHandler.Stream(ctx, func(event DiagnosticEvent, path string, diagnostics ...*ProtoDiagnostic) {
uri, err := c.resolver.PathToURI(path)
if err != nil {
return true // ???
return
}
rawResults, resultId, unchanged := dl.Get(maybePrevResultId...)
if unchanged {
callback(uri, []protocol.Diagnostic{}, protocol.DiagnosticUnchanged, resultId)
} else {
protocolResults := c.toProtocolDiagnostics(rawResults)

callback(uri, protocolResults, protocol.DiagnosticFull, resultId)
version := diagnosticVersions[uri]
version++
diagnosticVersions[uri] = version

switch event {
case DiagnosticEventAdd:
protocolDiagnostics := c.toProtocolDiagnostics(diagnostics)
currentDiagnostics[uri] = append(currentDiagnostics[uri], protocolDiagnostics...)
ch <- protocol.WorkspaceDiagnosticReportPartialResult{
Items: []protocol.Or_WorkspaceDocumentDiagnosticReport{
{
Value: protocol.WorkspaceFullDocumentDiagnosticReport{
Version: version,
URI: protocol.URIFromSpanURI(uri),
FullDocumentDiagnosticReport: protocol.FullDocumentDiagnosticReport{
Kind: string(protocol.DiagnosticFull),
Items: currentDiagnostics[uri],
},
},
},
},
}
case DiagnosticEventClear:
delete(currentDiagnostics, uri)
ch <- protocol.WorkspaceDiagnosticReportPartialResult{
Items: []protocol.Or_WorkspaceDocumentDiagnosticReport{
{
Value: protocol.WorkspaceFullDocumentDiagnosticReport{
Version: version,
URI: protocol.URIFromSpanURI(uri),
FullDocumentDiagnosticReport: protocol.FullDocumentDiagnosticReport{
Kind: string(protocol.DiagnosticFull),
Items: []protocol.Diagnostic{},
},
},
},
},
}
}
return true
})
}

Expand Down
122 changes: 91 additions & 31 deletions pkg/lsp/diagnostics.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lsp

import (
"context"
"errors"
"fmt"
"os"
Expand All @@ -10,8 +11,7 @@ import (
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/linker"
"github.com/bufbuild/protocompile/reporter"
gsync "github.com/kralicky/gpkg/sync"
"go.uber.org/atomic"
"golang.org/x/exp/slices"
"golang.org/x/tools/gopls/pkg/lsp/protocol"
)

Expand All @@ -24,7 +24,7 @@ type ProtoDiagnostic struct {

func NewDiagnosticHandler() *DiagnosticHandler {
return &DiagnosticHandler{
modified: atomic.NewBool(false),
diagnostics: map[string]*DiagnosticList{},
}
}

Expand All @@ -35,6 +35,9 @@ type DiagnosticList struct {
}

func (dl *DiagnosticList) Add(d *ProtoDiagnostic) {
if d == nil {
panic("bug: DiagnosticList: attempted to add nil diagnostic")
}
dl.lock.Lock()
defer dl.lock.Unlock()
dl.Diagnostics = append(dl.Diagnostics, d)
Expand All @@ -44,10 +47,14 @@ func (dl *DiagnosticList) Add(d *ProtoDiagnostic) {
func (dl *DiagnosticList) Get(prevResultId ...string) (diagnostics []*ProtoDiagnostic, resultId string, unchanged bool) {
dl.lock.RLock()
defer dl.lock.RUnlock()
return dl.getLocked(prevResultId...)
}

func (dl *DiagnosticList) getLocked(prevResultId ...string) (diagnostics []*ProtoDiagnostic, resultId string, unchanged bool) {
if len(prevResultId) == 1 && dl.ResultId == prevResultId[0] {
return []*ProtoDiagnostic{}, dl.ResultId, true
}
return dl.Diagnostics, dl.ResultId, false
return slices.Clone(dl.Diagnostics), dl.ResultId, false
}

func (dl *DiagnosticList) Clear() []*ProtoDiagnostic {
Expand All @@ -63,9 +70,21 @@ func (dl *DiagnosticList) resetResultId() {
dl.ResultId = time.Now().Format(time.RFC3339Nano)
}

type (
DiagnosticEvent int
ListenerFunc = func(event DiagnosticEvent, path string, diagnostics ...*ProtoDiagnostic)
)

const (
DiagnosticEventAdd DiagnosticEvent = iota
DiagnosticEventClear
)

type DiagnosticHandler struct {
diagnostics gsync.Map[string, *DiagnosticList]
modified *atomic.Bool
diagnosticsMu sync.RWMutex
diagnostics map[string]*DiagnosticList
listenerMu sync.RWMutex
listener ListenerFunc
}

func tagsForError(err error) []protocol.DiagnosticTag {
Expand All @@ -77,6 +96,15 @@ func tagsForError(err error) []protocol.DiagnosticTag {
}
}

func (dr *DiagnosticHandler) getOrCreateDiagnosticListLocked(filename string) (dl *DiagnosticList, existing bool) {
dl, existing = dr.diagnostics[filename]
if !existing {
dl = &DiagnosticList{}
dr.diagnostics[filename] = dl
}
return
}

func (dr *DiagnosticHandler) HandleError(err reporter.ErrorWithPos) error {
if err == nil {
return nil
Expand All @@ -87,18 +115,23 @@ func (dr *DiagnosticHandler) HandleError(err reporter.ErrorWithPos) error {
pos := err.GetPosition()
filename := pos.Start().Filename

empty := DiagnosticList{
Diagnostics: []*ProtoDiagnostic{},
}
dl, _ := dr.diagnostics.LoadOrStore(filename, &empty)
dl.Add(&ProtoDiagnostic{
dr.diagnosticsMu.Lock()
dl, _ := dr.getOrCreateDiagnosticListLocked(filename)
dr.diagnosticsMu.Unlock()

newDiagnostic := &ProtoDiagnostic{
Pos: pos,
Severity: protocol.SeverityError,
Error: err.Unwrap(),
Tags: tagsForError(err),
})
}
dl.Add(newDiagnostic)

dr.modified.CompareAndSwap(false, true)
dr.listenerMu.RLock()
if dr.listener != nil {
dr.listener(DiagnosticEventAdd, filename, newDiagnostic)
}
dr.listenerMu.RUnlock()

return nil // allow the compiler to continue
}
Expand All @@ -113,22 +146,29 @@ func (dr *DiagnosticHandler) HandleWarning(err reporter.ErrorWithPos) {
pos := err.GetPosition()
filename := pos.Start().Filename

empty := DiagnosticList{
Diagnostics: []*ProtoDiagnostic{},
}
dl, _ := dr.diagnostics.LoadOrStore(filename, &empty)
dl.Add(&ProtoDiagnostic{
dr.diagnosticsMu.Lock()
dl, _ := dr.getOrCreateDiagnosticListLocked(filename)
dr.diagnosticsMu.Unlock()

newDiagnostic := &ProtoDiagnostic{
Pos: pos,
Severity: protocol.SeverityWarning,
Error: err.Unwrap(),
Tags: tagsForError(err),
})
}
dl.Add(newDiagnostic)

dr.modified.CompareAndSwap(false, true)
dr.listenerMu.RLock()
if dr.listener != nil {
dr.listener(DiagnosticEventAdd, filename, newDiagnostic)
}
dr.listenerMu.RUnlock()
}

func (dr *DiagnosticHandler) GetDiagnosticsForPath(path string, prevResultId ...string) ([]*ProtoDiagnostic, string, bool) {
dl, ok := dr.diagnostics.Load(path)
dr.diagnosticsMu.RLock()
defer dr.diagnosticsMu.RUnlock()
dl, ok := dr.diagnostics[path]
if !ok {
return []*ProtoDiagnostic{}, "", false
}
Expand All @@ -139,20 +179,40 @@ func (dr *DiagnosticHandler) GetDiagnosticsForPath(path string, prevResultId ...
}

func (dr *DiagnosticHandler) ClearDiagnosticsForPath(path string) {
dl, ok := dr.diagnostics.Load(path)
if !ok {
return
dr.diagnosticsMu.Lock()
defer dr.diagnosticsMu.Unlock()
var prev []*ProtoDiagnostic
if dl, ok := dr.diagnostics[path]; ok {
prev = dl.Clear()
}
prev := dl.Clear()

fmt.Printf("[diagnostic] clearing %d diagnostics for %s\n", len(prev), path)

dr.listenerMu.RLock()
if dr.listener != nil {
dr.listener(DiagnosticEventClear, path, prev...)
}
dr.listenerMu.RUnlock()
}

func (dr *DiagnosticHandler) MaybeRange(setup func(), fn func(string, *DiagnosticList) bool) bool {
if dr.modified.CompareAndSwap(true, false) {
setup()
dr.diagnostics.Range(fn)
return true
func (dr *DiagnosticHandler) Stream(ctx context.Context, callback ListenerFunc) {
dr.diagnosticsMu.RLock()

dr.listenerMu.Lock()
dr.listener = callback
dr.listenerMu.Unlock()

dr.listenerMu.RLock()
for path, dl := range dr.diagnostics {
callback(DiagnosticEventAdd, path, dl.Diagnostics...)
}
return false
dr.listenerMu.RUnlock()

dr.diagnosticsMu.RUnlock()

<-ctx.Done()

dr.listenerMu.Lock()
dr.listener = nil
dr.listenerMu.Unlock()
}
45 changes: 25 additions & 20 deletions pkg/lsp/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,29 +96,33 @@ func (r *Resolver) UpdateURIPathMappings(modifications []source.FileModification
zap.String("filename", filename),
zap.Error(err),
).Error("failed to lookup go module")
r.filePathsByURI[m.URI] = ""
continue
}
path := filepath.Join(mod, filepath.Base(filename))
r.filePathsByURI[m.URI] = path
case source.Close:
case source.Save:
case source.Change:
if m.OnDisk {
// check for go_package modification
existingPath := r.filePathsByURI[m.URI]
filename := m.URI.Filename()
mod, err := FastLookupGoModule(filename)
if err != nil {
r.lg.With(
zap.String("filename", filename),
zap.Error(err),
).Error("failed to lookup go module")
continue
}
updatedPath := filepath.Join(mod, filepath.Base(filename))
if updatedPath != existingPath {
r.filePathsByURI[m.URI] = updatedPath
r.fileURIsByPath[updatedPath] = m.URI
case source.Change, source.Save:
// check for go_package modification
existingPath := r.filePathsByURI[m.URI]
filename := m.URI.Filename()
mod, err := FastLookupGoModule(filename)
if err != nil {
r.lg.With(
zap.String("filename", filename),
zap.Error(err),
).Error("failed to lookup go module")
continue
}
updatedPath := filepath.Join(mod, filepath.Base(filename))
if updatedPath != existingPath {
r.lg.With(
zap.String("existingPath", existingPath),
zap.String("updatedPath", updatedPath),
).Debug("updating path mapping")
r.filePathsByURI[m.URI] = updatedPath
r.fileURIsByPath[updatedPath] = m.URI
if existingPath != "" {
delete(r.fileURIsByPath, existingPath)
}
}
Expand All @@ -130,6 +134,7 @@ func (r *Resolver) UpdateURIPathMappings(modifications []source.FileModification
zap.String("filename", filename),
zap.Error(err),
).Error("failed to lookup go module")
r.filePathsByURI[m.URI] = ""
continue
}
canonicalName := filepath.Join(goPkg, filepath.Base(filename))
Expand All @@ -150,8 +155,8 @@ func (r *Resolver) UpdateURIPathMappings(modifications []source.FileModification
// 3.5. Check if the path is a go module path containing generated code, but no proto sources
// 4. Check if the path is found in the global message cache
func (r *Resolver) FindFileByPath(path string) (protocompile.SearchResult, error) {
r.pathsMu.RLock()
defer r.pathsMu.RUnlock()
r.pathsMu.Lock()
defer r.pathsMu.Unlock()

if result, err := r.checkWellKnownImportPath(path); err == nil {
r.lg.With(zap.String("path", path)).Debug("resolved to well-known import path")
Expand Down
Loading

0 comments on commit f0c9a04

Please sign in to comment.