1package lsp
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"github.com/charmbracelet/crush/internal/app"
  8	"github.com/charmbracelet/crush/internal/config"
  9	"github.com/charmbracelet/crush/internal/csync"
 10	"github.com/charmbracelet/crush/internal/lsp"
 11	"github.com/charmbracelet/crush/internal/tui/components/core"
 12	"github.com/charmbracelet/crush/internal/tui/styles"
 13	"github.com/charmbracelet/lipgloss/v2"
 14	"github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
 15)
 16
 17// RenderOptions contains options for rendering LSP lists.
 18type RenderOptions struct {
 19	MaxWidth    int
 20	MaxItems    int
 21	ShowSection bool
 22	SectionName string
 23}
 24
 25// RenderLSPList renders a list of LSP status items with the given options.
 26func RenderLSPList(lspClients *csync.Map[string, *lsp.Client], opts RenderOptions) []string {
 27	t := styles.CurrentTheme()
 28	lspList := []string{}
 29
 30	if opts.ShowSection {
 31		sectionName := opts.SectionName
 32		if sectionName == "" {
 33			sectionName = "LSPs"
 34		}
 35		section := t.S().Subtle.Render(sectionName)
 36		lspList = append(lspList, section, "")
 37	}
 38
 39	lspConfigs := config.Get().LSP.Sorted()
 40	if len(lspConfigs) == 0 {
 41		lspList = append(lspList, t.S().Base.Foreground(t.Border).Render("None"))
 42		return lspList
 43	}
 44
 45	// Get LSP states
 46	lspStates := app.GetLSPStates()
 47
 48	// Determine how many items to show
 49	maxItems := len(lspConfigs)
 50	if opts.MaxItems > 0 {
 51		maxItems = min(opts.MaxItems, len(lspConfigs))
 52	}
 53
 54	for i, l := range lspConfigs {
 55		if i >= maxItems {
 56			break
 57		}
 58
 59		icon, description := iconAndDescription(l, t, lspStates)
 60
 61		// Calculate diagnostic counts if we have LSP clients
 62		var extraContent string
 63		if lspClients != nil {
 64			lspErrs := map[protocol.DiagnosticSeverity]int{
 65				protocol.SeverityError:       0,
 66				protocol.SeverityWarning:     0,
 67				protocol.SeverityHint:        0,
 68				protocol.SeverityInformation: 0,
 69			}
 70			if client, ok := lspClients.Get(l.Name); ok {
 71				for _, diagnostics := range client.GetDiagnostics() {
 72					for _, diagnostic := range diagnostics {
 73						if severity, ok := lspErrs[diagnostic.Severity]; ok {
 74							lspErrs[diagnostic.Severity] = severity + 1
 75						}
 76					}
 77				}
 78			}
 79
 80			errs := []string{}
 81			if lspErrs[protocol.SeverityError] > 0 {
 82				errs = append(errs, t.S().Base.Foreground(t.Error).Render(fmt.Sprintf("%s %d", styles.ErrorIcon, lspErrs[protocol.SeverityError])))
 83			}
 84			if lspErrs[protocol.SeverityWarning] > 0 {
 85				errs = append(errs, t.S().Base.Foreground(t.Warning).Render(fmt.Sprintf("%s %d", styles.WarningIcon, lspErrs[protocol.SeverityWarning])))
 86			}
 87			if lspErrs[protocol.SeverityHint] > 0 {
 88				errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s %d", styles.HintIcon, lspErrs[protocol.SeverityHint])))
 89			}
 90			if lspErrs[protocol.SeverityInformation] > 0 {
 91				errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s %d", styles.InfoIcon, lspErrs[protocol.SeverityInformation])))
 92			}
 93			extraContent = strings.Join(errs, " ")
 94		}
 95
 96		lspList = append(lspList,
 97			core.Status(
 98				core.StatusOpts{
 99					Icon:         icon.String(),
100					Title:        l.Name,
101					Description:  description,
102					ExtraContent: extraContent,
103				},
104				opts.MaxWidth,
105			),
106		)
107	}
108
109	return lspList
110}
111
112func iconAndDescription(l config.LSP, t *styles.Theme, states map[string]app.LSPClientInfo) (lipgloss.Style, string) {
113	if l.LSP.Disabled {
114		return t.ItemOfflineIcon.Foreground(t.FgMuted), t.S().Subtle.Render("disabled")
115	}
116
117	info := states[l.Name]
118	switch info.State {
119	case lsp.StateStarting:
120		return t.ItemBusyIcon, t.S().Subtle.Render("starting...")
121	case lsp.StateReady:
122		return t.ItemOnlineIcon, ""
123	case lsp.StateError:
124		description := t.S().Subtle.Render("error")
125		if info.Error != nil {
126			description = t.S().Subtle.Render(fmt.Sprintf("error: %s", info.Error.Error()))
127		}
128		return t.ItemErrorIcon, description
129	case lsp.StateDisabled:
130		return t.ItemOfflineIcon.Foreground(t.FgMuted), t.S().Subtle.Render("inactive")
131	default:
132		return t.ItemOfflineIcon, ""
133	}
134}
135
136// RenderLSPBlock renders a complete LSP block with optional truncation indicator.
137func RenderLSPBlock(lspClients *csync.Map[string, *lsp.Client], opts RenderOptions, showTruncationIndicator bool) string {
138	t := styles.CurrentTheme()
139	lspList := RenderLSPList(lspClients, opts)
140
141	// Add truncation indicator if needed
142	if showTruncationIndicator && opts.MaxItems > 0 {
143		lspConfigs := config.Get().LSP.Sorted()
144		if len(lspConfigs) > opts.MaxItems {
145			remaining := len(lspConfigs) - opts.MaxItems
146			if remaining == 1 {
147				lspList = append(lspList, t.S().Base.Foreground(t.FgMuted).Render("β¦"))
148			} else {
149				lspList = append(lspList,
150					t.S().Base.Foreground(t.FgSubtle).Render(fmt.Sprintf("β¦and %d more", remaining)),
151				)
152			}
153		}
154	}
155
156	content := lipgloss.JoinVertical(lipgloss.Left, lspList...)
157	if opts.MaxWidth > 0 {
158		return lipgloss.NewStyle().Width(opts.MaxWidth).Render(content)
159	}
160	return content
161}