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}