diff --git a/protols/cache.go b/protols/cache.go index bb165cd..1e69904 100644 --- a/protols/cache.go +++ b/protols/cache.go @@ -625,39 +625,67 @@ func (c *Cache) getMapper(uri span.URI) (*protocol.Mapper, error) { return c.compiler.overlay.Get(c.filePathsByURI[uri]) } -func (c *Cache) ComputeDiagnosticReports(uri span.URI) ([]*protocol.Diagnostic, error) { +func (c *Cache) ComputeDiagnosticReports(uri span.URI, prevResultId string) ([]protocol.Diagnostic, protocol.DocumentDiagnosticReportKind, string, error) { c.resultsMu.Lock() defer c.resultsMu.Unlock() - rawReports, found := c.diagHandler.GetDiagnosticsForPath(c.filePathsByURI[uri]) - if !found { - return nil, nil // no reports + var maybePrevResultId []string + if prevResultId != "" { + maybePrevResultId = append(maybePrevResultId, prevResultId) } - mapper, err := c.getMapper(uri) - if err != nil { - return nil, err + rawReports, resultId, unchanged := c.diagHandler.GetDiagnosticsForPath(c.filePathsByURI[uri], maybePrevResultId...) + if unchanged { + return nil, protocol.DiagnosticUnchanged, resultId, nil } + protocolReports := c.toProtocolDiagnostics(rawReports) + + return protocolReports, protocol.DiagnosticFull, resultId, nil +} - // convert to protocol reports - var reports []*protocol.Diagnostic +func (c *Cache) toProtocolDiagnostics(rawReports []*ProtoDiagnostic) []protocol.Diagnostic { + var reports []protocol.Diagnostic for _, rawReport := range rawReports { - rng, err := mapper.OffsetRange(rawReport.Pos.Start().Offset, rawReport.Pos.End().Offset+1) - if err != nil { - c.lg.With( - zap.String("file", string(uri)), - zap.Error(err), - ).Debug("failed to map range") - continue - } - reports = append(reports, &protocol.Diagnostic{ - Range: rng, + reports = append(reports, protocol.Diagnostic{ + Range: toRange(rawReport.Pos), Severity: rawReport.Severity, Message: rawReport.Error.Error(), Tags: rawReport.Tags, Source: "protols", }) } + return reports +} + +type workspaceDiagnosticCallbackFunc = func(uri span.URI, reports []protocol.Diagnostic, kind protocol.DocumentDiagnosticReportKind, resultId string) - return reports, nil +func (c *Cache) ComputeWorkspaceDiagnosticReports(ctx context.Context, previousResultIds []protocol.PreviousResultID, callback workspaceDiagnosticCallbackFunc) bool { + 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.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.PathToURI(s) + if err != nil { + return true // ??? + } + 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) + } + return true + }) } func (c *Cache) ComputeDocumentLinks(doc protocol.TextDocumentIdentifier) ([]protocol.DocumentLink, error) { diff --git a/protols/diagnostics.go b/protols/diagnostics.go index deddc4f..6730d6b 100644 --- a/protols/diagnostics.go +++ b/protols/diagnostics.go @@ -5,10 +5,13 @@ import ( "fmt" "os" "sync" + "time" "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/tools/gopls/pkg/lsp/protocol" ) @@ -21,13 +24,48 @@ type ProtoDiagnostic struct { func NewDiagnosticHandler() *DiagnosticHandler { return &DiagnosticHandler{ - diagnostics: make(map[string][]*ProtoDiagnostic), + modified: atomic.NewBool(false), } } +type DiagnosticList struct { + lock sync.RWMutex + Diagnostics []*ProtoDiagnostic + ResultId string +} + +func (dl *DiagnosticList) Add(d *ProtoDiagnostic) { + dl.lock.Lock() + defer dl.lock.Unlock() + dl.Diagnostics = append(dl.Diagnostics, d) + dl.resetResultId() +} + +func (dl *DiagnosticList) Get(prevResultId ...string) (diagnostics []*ProtoDiagnostic, resultId string, unchanged bool) { + dl.lock.RLock() + defer dl.lock.RUnlock() + if len(prevResultId) == 1 && dl.ResultId == prevResultId[0] { + return []*ProtoDiagnostic{}, dl.ResultId, true + } + return dl.Diagnostics, dl.ResultId, false +} + +func (dl *DiagnosticList) Clear() []*ProtoDiagnostic { + dl.lock.Lock() + defer dl.lock.Unlock() + dl.Diagnostics = []*ProtoDiagnostic{} + dl.resetResultId() + return dl.Diagnostics +} + +// requires lock to be held in write mode +func (dl *DiagnosticList) resetResultId() { + dl.ResultId = time.Now().Format(time.RFC3339Nano) +} + type DiagnosticHandler struct { - diagnosticsMu sync.Mutex - diagnostics map[string][]*ProtoDiagnostic + diagnostics gsync.Map[string, *DiagnosticList] + modified *atomic.Bool } func tagsForError(err error) []protocol.DiagnosticTag { @@ -35,7 +73,7 @@ func tagsForError(err error) []protocol.DiagnosticTag { case linker.ErrorUnusedImport: return []protocol.DiagnosticTag{protocol.Unnecessary} default: - return nil + return []protocol.DiagnosticTag{} } } @@ -46,19 +84,22 @@ func (dr *DiagnosticHandler) HandleError(err reporter.ErrorWithPos) error { fmt.Fprintf(os.Stderr, "[diagnostic] error: %s\n", err.Error()) - dr.diagnosticsMu.Lock() - defer dr.diagnosticsMu.Unlock() - pos := err.GetPosition() filename := pos.Start().Filename - dr.diagnostics[filename] = append(dr.diagnostics[filename], &ProtoDiagnostic{ + empty := DiagnosticList{ + Diagnostics: []*ProtoDiagnostic{}, + } + dl, _ := dr.diagnostics.LoadOrStore(filename, &empty) + dl.Add(&ProtoDiagnostic{ Pos: pos, Severity: protocol.SeverityError, Error: err.Unwrap(), Tags: tagsForError(err), }) + dr.modified.CompareAndSwap(false, true) + return nil // allow the compiler to continue } @@ -69,35 +110,50 @@ func (dr *DiagnosticHandler) HandleWarning(err reporter.ErrorWithPos) { fmt.Fprintf(os.Stderr, "[diagnostic] error: %s\n", err.Error()) - dr.diagnosticsMu.Lock() - defer dr.diagnosticsMu.Unlock() - pos := err.GetPosition() filename := pos.Start().Filename - dr.diagnostics[filename] = append(dr.diagnostics[filename], &ProtoDiagnostic{ + empty := DiagnosticList{ + Diagnostics: []*ProtoDiagnostic{}, + } + dl, _ := dr.diagnostics.LoadOrStore(filename, &empty) + dl.Add(&ProtoDiagnostic{ Pos: pos, Severity: protocol.SeverityWarning, Error: err.Unwrap(), Tags: tagsForError(err), }) -} -func (dr *DiagnosticHandler) GetDiagnosticsForPath(path string) ([]*ProtoDiagnostic, bool) { - dr.diagnosticsMu.Lock() - defer dr.diagnosticsMu.Unlock() + dr.modified.CompareAndSwap(false, true) +} - res, ok := dr.diagnostics[path] +func (dr *DiagnosticHandler) GetDiagnosticsForPath(path string, prevResultId ...string) ([]*ProtoDiagnostic, string, bool) { + dl, ok := dr.diagnostics.Load(path) + if !ok { + return []*ProtoDiagnostic{}, "", false + } + return dl.Get(prevResultId...) - fmt.Printf("[diagnostic] querying diagnostics for %s: (%d results)\n", path, len(res)) - return res, ok + // fmt.Printf("[diagnostic] querying diagnostics for %s: (%d results)\n", path, len(res)) + // return res, ok } func (dr *DiagnosticHandler) ClearDiagnosticsForPath(path string) { - dr.diagnosticsMu.Lock() - defer dr.diagnosticsMu.Unlock() + dl, ok := dr.diagnostics.Load(path) + if !ok { + return + } + dl.Clear() - fmt.Printf("[diagnostic] clearing %d diagnostics for %s\n", len(dr.diagnostics[path]), path) + // fmt.Printf("[diagnostic] clearing %d diagnostics for %s\n", len(dr.diagnostics[path]), path) - delete(dr.diagnostics, path) +} + +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 + } + return false } diff --git a/protols/formatter.go b/protols/formatter.go index bdb8d51..c65bdad 100644 --- a/protols/formatter.go +++ b/protols/formatter.go @@ -721,6 +721,8 @@ func columnFormatElements[T ast.Node](f *formatter, elems []T) { field.typeName, _ = io.ReadAll(colBuf) // these column names don't exactly match up with the others fclone.writeLineEnd(elem.Val) field.lineEnd, _ = io.ReadAll(colBuf) + case *ast.OptionNode: + // TODO default: panic(fmt.Sprintf("column formatting not implemented for element type %T", elem)) diff --git a/protols/server.go b/protols/server.go index c460242..bb9577f 100644 --- a/protols/server.go +++ b/protols/server.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "errors" "fmt" @@ -12,6 +13,7 @@ import ( "github.com/samber/lo" "go.uber.org/zap" "golang.org/x/tools/gopls/pkg/lsp/protocol" + "golang.org/x/tools/gopls/pkg/span" "golang.org/x/tools/pkg/jsonrpc2" "google.golang.org/protobuf/reflect/protoreflect" ) @@ -53,14 +55,19 @@ func (s *Server) Initialize(ctx context.Context, params *protocol.ParamInitializ // WillSaveWaitUntil: true, Save: &protocol.SaveOptions{IncludeText: false}, }, + HoverProvider: &protocol.Or_ServerCapabilities_hoverProvider{Value: true}, DiagnosticProvider: &protocol.Or_ServerCapabilities_diagnosticProvider{ - Value: protocol.DiagnosticOptions{ - WorkspaceDiagnostics: false, - InterFileDependencies: false, + Value: protocol.DiagnosticRegistrationOptions{ + DiagnosticOptions: protocol.DiagnosticOptions{ + WorkspaceDiagnostics: true, + }, }, }, Workspace: &protocol.Workspace6Gn{ + WorkspaceFolders: &protocol.WorkspaceFolders5Gn{ + Supported: false, // TODO + }, FileOperations: &protocol.FileOperationOptions{ DidCreate: &protocol.FileOperationRegistrationOptions{ Filters: filters, @@ -101,7 +108,6 @@ func (s *Server) Initialize(ctx context.Context, params *protocol.ParamInitializ Full: &protocol.Or_SemanticTokensOptions_full{Value: true}, Range: &protocol.Or_SemanticTokensOptions_range{Value: true}, }, - // DocumentSymbolProvider: &protocol.Or_ServerCapabilities_documentSymbolProvider{Value: true}, }, @@ -390,23 +396,36 @@ func (*Server) Declaration(context.Context, *protocol.DeclarationParams) (*proto // Diagnostic implements protocol.Server. func (s *Server) Diagnostic(ctx context.Context, params *protocol.DocumentDiagnosticParams) (*protocol.Or_DocumentDiagnosticReport, error) { - reports, err := s.c.ComputeDiagnosticReports(params.TextDocument.URI.SpanURI()) + reports, kind, resultId, err := s.c.ComputeDiagnosticReports(params.TextDocument.URI.SpanURI(), params.PreviousResultID) if err != nil { s.lg.Error("failed to compute diagnostic reports", zap.Error(err)) return nil, err } - items := []protocol.Diagnostic{} - for _, report := range reports { - items = append(items, *report) - } - return &protocol.Or_DocumentDiagnosticReport{ - Value: protocol.RelatedFullDocumentDiagnosticReport{ - FullDocumentDiagnosticReport: protocol.FullDocumentDiagnosticReport{ - Kind: string(protocol.DiagnosticFull), - Items: items, + switch kind { + case protocol.DiagnosticFull: + return &protocol.Or_DocumentDiagnosticReport{ + Value: protocol.RelatedFullDocumentDiagnosticReport{ + RelatedDocuments: map[protocol.DocumentURI]interface{}{}, + FullDocumentDiagnosticReport: protocol.FullDocumentDiagnosticReport{ + Kind: string(protocol.DiagnosticFull), + ResultID: resultId, + Items: reports, + }, }, - }, - }, nil + }, nil + case protocol.DiagnosticUnchanged: + return &protocol.Or_DocumentDiagnosticReport{ + Value: protocol.RelatedUnchangedDocumentDiagnosticReport{ + RelatedDocuments: map[protocol.DocumentURI]interface{}{}, + UnchangedDocumentDiagnosticReport: protocol.UnchangedDocumentDiagnosticReport{ + Kind: string(protocol.DiagnosticUnchanged), + ResultID: resultId, + }, + }, + }, nil + default: + panic("bug: unknown diagnostic kind: " + kind) + } } // DiagnosticRefresh implements protocol.Server. @@ -415,8 +434,50 @@ func (*Server) DiagnosticRefresh(context.Context) error { } // DiagnosticWorkspace implements protocol.Server. -func (*Server) DiagnosticWorkspace(context.Context, *protocol.WorkspaceDiagnosticParams) (*protocol.WorkspaceDiagnosticReport, error) { - return nil, jsonrpc2.ErrMethodNotFound +func (s *Server) DiagnosticWorkspace(ctx context.Context, params *protocol.WorkspaceDiagnosticParams) (*protocol.WorkspaceDiagnosticReport, error) { + report := &protocol.WorkspaceDiagnosticReport{ + Items: []protocol.Or_WorkspaceDocumentDiagnosticReport{}, + } + + ok := s.c.ComputeWorkspaceDiagnosticReports(ctx, params.PreviousResultIds, + func(uri span.URI, reports []protocol.Diagnostic, kind protocol.DocumentDiagnosticReportKind, resultId string) { + + if kind == protocol.DiagnosticUnchanged { + report.Items = append(report.Items, protocol.Or_WorkspaceDocumentDiagnosticReport{ + Value: protocol.WorkspaceUnchangedDocumentDiagnosticReport{ + URI: protocol.URIFromSpanURI(uri), + UnchangedDocumentDiagnosticReport: protocol.UnchangedDocumentDiagnosticReport{ + Kind: string(protocol.DiagnosticUnchanged), + ResultID: resultId, + }, + }, + }) + return + } + + report.Items = append(report.Items, protocol.Or_WorkspaceDocumentDiagnosticReport{ + Value: protocol.WorkspaceFullDocumentDiagnosticReport{ + URI: protocol.URIFromSpanURI(uri), + FullDocumentDiagnosticReport: protocol.FullDocumentDiagnosticReport{ + Kind: string(protocol.DiagnosticFull), + ResultID: resultId, + Items: reports, + }, + }, + }) + }, + ) + + if ok { + return report, nil + } + + // TODO: this doesn't seem to work + // https://github.com/microsoft/vscode-languageserver-node/issues/1261 + data, _ := json.Marshal(protocol.DiagnosticServerCancellationData{ + RetriggerRequest: false, + }) + return nil, jsonrpc2.NewErrorWithData(int64(protocol.ServerCancelled), "", json.RawMessage(data)) } // DidChangeConfiguration implements protocol.Server.