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