From fcf9cd349517305d4ca2a903e7b5f760e0815d16 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Fri, 9 Jan 2026 11:39:38 -0300 Subject: [PATCH] perf: cache lsp diagnostic counts Signed-off-by: Carlos Alexandro Becker --- internal/lsp/client.go | 42 +++++++++++++++++++ internal/tui/components/chat/header/header.go | 9 +--- internal/tui/components/lsp/lsp.go | 42 +++++++------------ internal/ui/model/lsp.go | 17 +++----- 4 files changed, 62 insertions(+), 48 deletions(-) diff --git a/internal/lsp/client.go b/internal/lsp/client.go index 7f88598eec6ca84697bdfc2e893c1b42c973e26c..7d555e3e082721f3bd778a0901d5d1bb80afd880 100644 --- a/internal/lsp/client.go +++ b/internal/lsp/client.go @@ -21,6 +21,14 @@ import ( "github.com/charmbracelet/x/powernap/pkg/transport" ) +// DiagnosticCounts holds the count of diagnostics by severity. +type DiagnosticCounts struct { + Error int + Warning int + Information int + Hint int +} + type Client struct { client *powernap.Client name string @@ -37,6 +45,10 @@ type Client struct { // Diagnostic cache diagnostics *csync.VersionedMap[protocol.DocumentURI, []protocol.Diagnostic] + // Cached diagnostic counts to avoid map copy on every UI render. + diagCountsCache DiagnosticCounts + diagCountsVersion uint64 + // Files are currently opened by the LSP openFiles *csync.Map[string, *OpenFileInfo] @@ -353,6 +365,36 @@ func (c *Client) GetDiagnostics() map[protocol.DocumentURI][]protocol.Diagnostic return c.diagnostics.Copy() } +// GetDiagnosticCounts returns cached diagnostic counts by severity. +// Uses the VersionedMap version to avoid recomputing on every call. +func (c *Client) GetDiagnosticCounts() DiagnosticCounts { + currentVersion := c.diagnostics.Version() + if currentVersion == c.diagCountsVersion { + return c.diagCountsCache + } + + // Recompute counts. + counts := DiagnosticCounts{} + for _, diags := range c.diagnostics.Seq2() { + for _, diag := range diags { + switch diag.Severity { + case protocol.SeverityError: + counts.Error++ + case protocol.SeverityWarning: + counts.Warning++ + case protocol.SeverityInformation: + counts.Information++ + case protocol.SeverityHint: + counts.Hint++ + } + } + } + + c.diagCountsCache = counts + c.diagCountsVersion = currentVersion + return counts +} + // OpenFileOnDemand opens a file only if it's not already open. func (c *Client) OpenFileOnDemand(ctx context.Context, filepath string) error { // Check if the file is already open diff --git a/internal/tui/components/chat/header/header.go b/internal/tui/components/chat/header/header.go index 59389815ac63ac127ac000abf872b000eb8f2347..c8848440b1193fda9a7b5df4b31e03edeaf744c4 100644 --- a/internal/tui/components/chat/header/header.go +++ b/internal/tui/components/chat/header/header.go @@ -15,7 +15,6 @@ import ( "github.com/charmbracelet/crush/internal/tui/styles" "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/x/ansi" - "github.com/charmbracelet/x/powernap/pkg/lsp/protocol" ) type Header interface { @@ -106,13 +105,7 @@ func (h *header) details(availWidth int) string { errorCount := 0 for l := range h.lspClients.Seq() { - for _, diagnostics := range l.GetDiagnostics() { - for _, diagnostic := range diagnostics { - if diagnostic.Severity == protocol.SeverityError { - errorCount++ - } - } - } + errorCount += l.GetDiagnosticCounts().Error } if errorCount > 0 { diff --git a/internal/tui/components/lsp/lsp.go b/internal/tui/components/lsp/lsp.go index 18c3f74b71768b88d068093759245615d2f7a284..f9118143cbfd9a7bf19aa569bc85448746debecd 100644 --- a/internal/tui/components/lsp/lsp.go +++ b/internal/tui/components/lsp/lsp.go @@ -11,7 +11,6 @@ import ( "github.com/charmbracelet/crush/internal/lsp" "github.com/charmbracelet/crush/internal/tui/components/core" "github.com/charmbracelet/crush/internal/tui/styles" - "github.com/charmbracelet/x/powernap/pkg/lsp/protocol" ) // RenderOptions contains options for rendering LSP lists. @@ -61,36 +60,23 @@ func RenderLSPList(lspClients *csync.Map[string, *lsp.Client], opts RenderOption // Calculate diagnostic counts if we have LSP clients var extraContent string if lspClients != nil { - lspErrs := map[protocol.DiagnosticSeverity]int{ - protocol.SeverityError: 0, - protocol.SeverityWarning: 0, - protocol.SeverityHint: 0, - protocol.SeverityInformation: 0, - } if client, ok := lspClients.Get(l.Name); ok { - for _, diagnostics := range client.GetDiagnostics() { - for _, diagnostic := range diagnostics { - if severity, ok := lspErrs[diagnostic.Severity]; ok { - lspErrs[diagnostic.Severity] = severity + 1 - } - } + counts := client.GetDiagnosticCounts() + errs := []string{} + if counts.Error > 0 { + errs = append(errs, t.S().Base.Foreground(t.Error).Render(fmt.Sprintf("%s %d", styles.ErrorIcon, counts.Error))) } + if counts.Warning > 0 { + errs = append(errs, t.S().Base.Foreground(t.Warning).Render(fmt.Sprintf("%s %d", styles.WarningIcon, counts.Warning))) + } + if counts.Hint > 0 { + errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s %d", styles.HintIcon, counts.Hint))) + } + if counts.Information > 0 { + errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s %d", styles.InfoIcon, counts.Information))) + } + extraContent = strings.Join(errs, " ") } - - errs := []string{} - if lspErrs[protocol.SeverityError] > 0 { - errs = append(errs, t.S().Base.Foreground(t.Error).Render(fmt.Sprintf("%s %d", styles.ErrorIcon, lspErrs[protocol.SeverityError]))) - } - if lspErrs[protocol.SeverityWarning] > 0 { - errs = append(errs, t.S().Base.Foreground(t.Warning).Render(fmt.Sprintf("%s %d", styles.WarningIcon, lspErrs[protocol.SeverityWarning]))) - } - if lspErrs[protocol.SeverityHint] > 0 { - errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s %d", styles.HintIcon, lspErrs[protocol.SeverityHint]))) - } - if lspErrs[protocol.SeverityInformation] > 0 { - errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s %d", styles.InfoIcon, lspErrs[protocol.SeverityInformation]))) - } - extraContent = strings.Join(errs, " ") } lspList = append(lspList, diff --git a/internal/ui/model/lsp.go b/internal/ui/model/lsp.go index b1a3b8ebb223ce20687a0885f21a65a7ed1bf88a..1f13b5afc3c8a90b6ca14e304636e31fbedddbfc 100644 --- a/internal/ui/model/lsp.go +++ b/internal/ui/model/lsp.go @@ -29,19 +29,12 @@ func (m *UI) lspInfo(width, maxItems int, isSection bool) string { if !ok { continue } + counts := client.GetDiagnosticCounts() lspErrs := map[protocol.DiagnosticSeverity]int{ - protocol.SeverityError: 0, - protocol.SeverityWarning: 0, - protocol.SeverityHint: 0, - protocol.SeverityInformation: 0, - } - - for _, diagnostics := range client.GetDiagnostics() { - for _, diagnostic := range diagnostics { - if severity, ok := lspErrs[diagnostic.Severity]; ok { - lspErrs[diagnostic.Severity] = severity + 1 - } - } + protocol.SeverityError: counts.Error, + protocol.SeverityWarning: counts.Warning, + protocol.SeverityHint: counts.Hint, + protocol.SeverityInformation: counts.Information, } lsps = append(lsps, LSPInfo{LSPClientInfo: state, Diagnostics: lspErrs})