lsp.go

  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/config"
 11	"github.com/charmbracelet/crush/internal/lsp"
 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, cfg *config.Config, 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 := cfg.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())
 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(), 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, cfg *config.Config, opts RenderOptions, showTruncationIndicator bool) string {
149	t := styles.CurrentTheme()
150	lspList := RenderLSPList(c, cfg, opts)
151	cfg, err := c.GetConfig(context.TODO())
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}