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