perf: cache lsp diagnostic counts

Carlos Alexandro Becker created

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

Change summary

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(-)

Detailed changes

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

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 {

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,

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})