lsp.go

  1package model
  2
  3import (
  4	"fmt"
  5	"maps"
  6	"slices"
  7	"strings"
  8
  9	"charm.land/lipgloss/v2"
 10	"github.com/charmbracelet/crush/internal/app"
 11	"github.com/charmbracelet/crush/internal/lsp"
 12	"github.com/charmbracelet/crush/internal/ui/common"
 13	"github.com/charmbracelet/crush/internal/ui/styles"
 14	"github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
 15)
 16
 17// LSPInfo wraps LSP client information with diagnostic counts by severity.
 18type LSPInfo struct {
 19	app.LSPClientInfo
 20	Diagnostics map[protocol.DiagnosticSeverity]int
 21}
 22
 23// lspInfo renders the LSP status section showing active LSP clients and their
 24// diagnostic counts.
 25func (m *UI) lspInfo(width, maxItems int, isSection bool) string {
 26	t := m.com.Styles
 27
 28	states := slices.SortedFunc(maps.Values(m.lspStates), func(a, b app.LSPClientInfo) int {
 29		return strings.Compare(a.Name, b.Name)
 30	})
 31
 32	var lsps []LSPInfo
 33	for _, state := range states {
 34		lspErrs := map[protocol.DiagnosticSeverity]int{}
 35		if client, ok := m.com.App.LSPManager.Clients().Get(state.Name); ok {
 36			counts := client.GetDiagnosticCounts()
 37			lspErrs[protocol.SeverityError] = counts.Error
 38			lspErrs[protocol.SeverityWarning] = counts.Warning
 39			lspErrs[protocol.SeverityHint] = counts.Hint
 40			lspErrs[protocol.SeverityInformation] = counts.Information
 41		}
 42
 43		lsps = append(lsps, LSPInfo{LSPClientInfo: state, Diagnostics: lspErrs})
 44	}
 45
 46	title := t.ResourceGroupTitle.Render("LSPs")
 47	if isSection {
 48		title = common.Section(t, title, width)
 49	}
 50	list := t.ResourceAdditionalText.Render("None")
 51	if len(lsps) > 0 {
 52		list = lspList(t, lsps, width, maxItems)
 53	}
 54
 55	return lipgloss.NewStyle().Width(width).Render(fmt.Sprintf("%s\n\n%s", title, list))
 56}
 57
 58// lspDiagnostics formats diagnostic counts with appropriate icons and colors.
 59func lspDiagnostics(t *styles.Styles, diagnostics map[protocol.DiagnosticSeverity]int) string {
 60	var errs []string
 61	if diagnostics[protocol.SeverityError] > 0 {
 62		errs = append(errs, t.LSP.ErrorDiagnostic.Render(fmt.Sprintf("%s%d", styles.LSPErrorIcon, diagnostics[protocol.SeverityError])))
 63	}
 64	if diagnostics[protocol.SeverityWarning] > 0 {
 65		errs = append(errs, t.LSP.WarningDiagnostic.Render(fmt.Sprintf("%s%d", styles.LSPWarningIcon, diagnostics[protocol.SeverityWarning])))
 66	}
 67	if diagnostics[protocol.SeverityHint] > 0 {
 68		errs = append(errs, t.LSP.HintDiagnostic.Render(fmt.Sprintf("%s%d", styles.LSPHintIcon, diagnostics[protocol.SeverityHint])))
 69	}
 70	if diagnostics[protocol.SeverityInformation] > 0 {
 71		errs = append(errs, t.LSP.InfoDiagnostic.Render(fmt.Sprintf("%s%d", styles.LSPInfoIcon, diagnostics[protocol.SeverityInformation])))
 72	}
 73	return strings.Join(errs, " ")
 74}
 75
 76// lspList renders a list of LSP clients with their status and diagnostics,
 77// truncating to maxItems if needed.
 78func lspList(t *styles.Styles, lsps []LSPInfo, width, maxItems int) string {
 79	if maxItems <= 0 {
 80		return ""
 81	}
 82	var renderedLsps []string
 83	for _, l := range lsps {
 84		var icon string
 85		title := t.ResourceName.Render(l.Name)
 86		var description string
 87		var diagnostics string
 88		switch l.State {
 89		case lsp.StateUnstarted:
 90			icon = t.ResourceOfflineIcon.String()
 91			description = t.ResourceStatus.Render("unstarted")
 92		case lsp.StateStopped:
 93			icon = t.ResourceOfflineIcon.String()
 94			description = t.ResourceStatus.Render("stopped")
 95		case lsp.StateStarting:
 96			icon = t.ResourceBusyIcon.String()
 97			description = t.ResourceStatus.Render("starting...")
 98		case lsp.StateReady:
 99			icon = t.ResourceOnlineIcon.String()
100			diagnostics = lspDiagnostics(t, l.Diagnostics)
101		case lsp.StateError:
102			icon = t.ResourceErrorIcon.String()
103			description = t.ResourceStatus.Render("error")
104			if l.Error != nil {
105				description = t.ResourceStatus.Render(fmt.Sprintf("error: %s", l.Error.Error()))
106			}
107		case lsp.StateDisabled:
108			icon = t.ResourceOfflineIcon.Foreground(t.Muted.GetBackground()).String()
109			description = t.ResourceStatus.Render("disabled")
110		default:
111			continue
112		}
113		renderedLsps = append(renderedLsps, common.Status(t, common.StatusOpts{
114			Icon:         icon,
115			Title:        title,
116			Description:  description,
117			ExtraContent: diagnostics,
118		}, width))
119	}
120
121	if len(renderedLsps) > maxItems {
122		visibleItems := renderedLsps[:maxItems-1]
123		remaining := len(renderedLsps) - maxItems
124		visibleItems = append(visibleItems, t.ResourceAdditionalText.Render(fmt.Sprintf("…and %d more", remaining)))
125		return lipgloss.JoinVertical(lipgloss.Left, visibleItems...)
126	}
127	return lipgloss.JoinVertical(lipgloss.Left, renderedLsps...)
128}