diff --git a/internal/app/app.go b/internal/app/app.go index ba955e311e6a22b89bbe44d64fc7f1bfb01d8850..e923a0337f125cab17192e94ef61e17d23ae6582 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -126,9 +126,14 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) { // Set up callback for LSP state updates. app.LSPManager.SetCallback(func(name string, client *lsp.Client) { + if client == nil { + updateLSPState(name, lsp.StateUnstarted, nil, nil, 0) + return + } client.SetDiagnosticsCallback(updateLSPDiagnostics) updateLSPState(name, client.GetServerState(), nil, client, 0) }) + go app.LSPManager.TrackConfigured() return app, nil } diff --git a/internal/lsp/client.go b/internal/lsp/client.go index d8a97a429ea2f3a60e600731c6343a52c51e992b..c82dffabf40e99dc932e2fd326b24031d3e04ebb 100644 --- a/internal/lsp/client.go +++ b/internal/lsp/client.go @@ -241,10 +241,11 @@ func (c *Client) Restart() error { type ServerState int const ( - StateStopped ServerState = iota + StateUnstarted ServerState = iota StateStarting StateReady StateError + StateStopped StateDisabled ) diff --git a/internal/lsp/manager.go b/internal/lsp/manager.go index 88f9d72972350106c9ffc52d85434b0f20ec33aa..efa7596a685786e3a3c4053eb94f8858c7549e9f 100644 --- a/internal/lsp/manager.go +++ b/internal/lsp/manager.go @@ -77,6 +77,24 @@ func (s *Manager) SetCallback(cb func(name string, client *Client)) { s.callback = cb } +// TrackConfigured will callback the user-configured LSPs, but will not create +// any clients. +func (s *Manager) TrackConfigured() { + s.mu.Lock() + defer s.mu.Unlock() + + var wg sync.WaitGroup + for name := range s.manager.GetServers() { + if !s.isUserConfigured(name) { + continue + } + wg.Go(func() { + s.callback(name, nil) + }) + } + wg.Wait() +} + // Start starts an LSP server that can handle the given file path. // If an appropriate LSP is already running, this is a no-op. func (s *Manager) Start(ctx context.Context, path string) { diff --git a/internal/ui/model/lsp.go b/internal/ui/model/lsp.go index 9566e7b28403685e4a961e01158cfbf027d5e156..87de0d39d20520b3b24f5da3861efe7d5f9fe4a5 100644 --- a/internal/ui/model/lsp.go +++ b/internal/ui/model/lsp.go @@ -31,26 +31,23 @@ func (m *UI) lspInfo(width, maxItems int, isSection bool) string { var lsps []LSPInfo for _, state := range states { - client, ok := m.com.App.LSPManager.Clients().Get(state.Name) - if !ok { - continue - } - counts := client.GetDiagnosticCounts() - lspErrs := map[protocol.DiagnosticSeverity]int{ - protocol.SeverityError: counts.Error, - protocol.SeverityWarning: counts.Warning, - protocol.SeverityHint: counts.Hint, - protocol.SeverityInformation: counts.Information, + lspErrs := map[protocol.DiagnosticSeverity]int{} + if client, ok := m.com.App.LSPManager.Clients().Get(state.Name); ok { + counts := client.GetDiagnosticCounts() + lspErrs[protocol.SeverityError] = counts.Error + lspErrs[protocol.SeverityWarning] = counts.Warning + lspErrs[protocol.SeverityHint] = counts.Hint + lspErrs[protocol.SeverityInformation] = counts.Information } lsps = append(lsps, LSPInfo{LSPClientInfo: state, Diagnostics: lspErrs}) } - title := t.Subtle.Render("LSPs") + title := t.ResourceGroupTitle.Render("LSPs") if isSection { title = common.Section(t, title, width) } - list := t.Subtle.Render("None") + list := t.ResourceAdditionalText.Render("None") if len(lsps) > 0 { list = lspList(t, lsps, width, maxItems) } @@ -85,30 +82,33 @@ func lspList(t *styles.Styles, lsps []LSPInfo, width, maxItems int) string { var renderedLsps []string for _, l := range lsps { var icon string - title := l.Name + title := t.ResourceName.Render(l.Name) var description string var diagnostics string switch l.State { + case lsp.StateUnstarted: + icon = t.ResourceOfflineIcon.String() + description = t.ResourceStatus.Render("unstarted") case lsp.StateStopped: - icon = t.ItemOfflineIcon.Foreground(t.Muted.GetBackground()).String() - description = t.Subtle.Render("stopped") + icon = t.ResourceOfflineIcon.String() + description = t.ResourceStatus.Render("stopped") case lsp.StateStarting: - icon = t.ItemBusyIcon.String() - description = t.Subtle.Render("starting...") + icon = t.ResourceBusyIcon.String() + description = t.ResourceStatus.Render("starting...") case lsp.StateReady: - icon = t.ItemOnlineIcon.String() + icon = t.ResourceOnlineIcon.String() diagnostics = lspDiagnostics(t, l.Diagnostics) case lsp.StateError: - icon = t.ItemErrorIcon.String() - description = t.Subtle.Render("error") + icon = t.ResourceErrorIcon.String() + description = t.ResourceStatus.Render("error") if l.Error != nil { - description = t.Subtle.Render(fmt.Sprintf("error: %s", l.Error.Error())) + description = t.ResourceStatus.Render(fmt.Sprintf("error: %s", l.Error.Error())) } case lsp.StateDisabled: - icon = t.ItemOfflineIcon.Foreground(t.Muted.GetBackground()).String() - description = t.Subtle.Render("disabled") + icon = t.ResourceOfflineIcon.Foreground(t.Muted.GetBackground()).String() + description = t.ResourceStatus.Render("disabled") default: - icon = t.ItemOfflineIcon.String() + icon = t.ResourceOfflineIcon.String() } renderedLsps = append(renderedLsps, common.Status(t, common.StatusOpts{ Icon: icon, @@ -121,7 +121,7 @@ func lspList(t *styles.Styles, lsps []LSPInfo, width, maxItems int) string { if len(renderedLsps) > maxItems { visibleItems := renderedLsps[:maxItems-1] remaining := len(renderedLsps) - maxItems - visibleItems = append(visibleItems, t.Subtle.Render(fmt.Sprintf("…and %d more", remaining))) + visibleItems = append(visibleItems, t.ResourceAdditionalText.Render(fmt.Sprintf("…and %d more", remaining))) return lipgloss.JoinVertical(lipgloss.Left, visibleItems...) } return lipgloss.JoinVertical(lipgloss.Left, renderedLsps...) diff --git a/internal/ui/model/mcp.go b/internal/ui/model/mcp.go index 517016f0dcb9b5f237d4ac09c9816a290a42fdcc..c5c94268d2985fff3c79590d3f432872439962b2 100644 --- a/internal/ui/model/mcp.go +++ b/internal/ui/model/mcp.go @@ -22,11 +22,11 @@ func (m *UI) mcpInfo(width, maxItems int, isSection bool) string { } } - title := t.Subtle.Render("MCPs") + title := t.ResourceGroupTitle.Render("MCPs") if isSection { title = common.Section(t, title, width) } - list := t.Subtle.Render("None") + list := t.ResourceAdditionalText.Render("None") if len(mcps) > 0 { list = mcpList(t, mcps, width, maxItems) } @@ -59,28 +59,28 @@ func mcpList(t *styles.Styles, mcps []mcp.ClientInfo, width, maxItems int) strin for _, m := range mcps { var icon string - title := m.Name + title := t.ResourceName.Render(m.Name) var description string var extraContent string switch m.State { case mcp.StateStarting: - icon = t.ItemBusyIcon.String() - description = t.Subtle.Render("starting...") + icon = t.ResourceBusyIcon.String() + description = t.ResourceStatus.Render("starting...") case mcp.StateConnected: - icon = t.ItemOnlineIcon.String() + icon = t.ResourceOnlineIcon.String() extraContent = mcpCounts(t, m.Counts) case mcp.StateError: - icon = t.ItemErrorIcon.String() - description = t.Subtle.Render("error") + icon = t.ResourceErrorIcon.String() + description = t.ResourceStatus.Render("error") if m.Error != nil { - description = t.Subtle.Render(fmt.Sprintf("error: %s", m.Error.Error())) + description = t.ResourceStatus.Render(fmt.Sprintf("error: %s", m.Error.Error())) } case mcp.StateDisabled: - icon = t.ItemOfflineIcon.Foreground(t.Muted.GetBackground()).String() - description = t.Subtle.Render("disabled") + icon = t.ResourceOfflineIcon.Foreground(t.Muted.GetBackground()).String() + description = t.ResourceStatus.Render("disabled") default: - icon = t.ItemOfflineIcon.String() + icon = t.ResourceOfflineIcon.String() } renderedMcps = append(renderedMcps, common.Status(t, common.StatusOpts{ @@ -94,7 +94,7 @@ func mcpList(t *styles.Styles, mcps []mcp.ClientInfo, width, maxItems int) strin if len(renderedMcps) > maxItems { visibleItems := renderedMcps[:maxItems-1] remaining := len(renderedMcps) - maxItems - visibleItems = append(visibleItems, t.Subtle.Render(fmt.Sprintf("…and %d more", remaining))) + visibleItems = append(visibleItems, t.ResourceAdditionalText.Render(fmt.Sprintf("…and %d more", remaining))) return lipgloss.JoinVertical(lipgloss.Left, visibleItems...) } return lipgloss.JoinVertical(lipgloss.Left, renderedMcps...) diff --git a/internal/ui/styles/styles.go b/internal/ui/styles/styles.go index d28dd1b462ffa6e7bc6bc2c1a34b4ef66d513ef7..c0a15bc69ebd103adaa3d193645915184b64361a 100644 --- a/internal/ui/styles/styles.go +++ b/internal/ui/styles/styles.go @@ -109,10 +109,14 @@ type Styles struct { TextSelection lipgloss.Style // LSP and MCP status indicators - ItemOfflineIcon lipgloss.Style - ItemBusyIcon lipgloss.Style - ItemErrorIcon lipgloss.Style - ItemOnlineIcon lipgloss.Style + ResourceGroupTitle lipgloss.Style + ResourceOfflineIcon lipgloss.Style + ResourceBusyIcon lipgloss.Style + ResourceErrorIcon lipgloss.Style + ResourceOnlineIcon lipgloss.Style + ResourceName lipgloss.Style + ResourceStatus lipgloss.Style + ResourceAdditionalText lipgloss.Style // Markdown & Chroma Markdown ansi.StyleConfig @@ -1199,10 +1203,14 @@ func DefaultStyles() Styles { s.Initialize.Accent = s.Base.Foreground(greenDark) // LSP and MCP status. - s.ItemOfflineIcon = lipgloss.NewStyle().Foreground(charmtone.Squid).SetString("●") - s.ItemBusyIcon = s.ItemOfflineIcon.Foreground(charmtone.Citron) - s.ItemErrorIcon = s.ItemOfflineIcon.Foreground(charmtone.Coral) - s.ItemOnlineIcon = s.ItemOfflineIcon.Foreground(charmtone.Guac) + s.ResourceGroupTitle = lipgloss.NewStyle().Foreground(charmtone.Oyster) + s.ResourceOfflineIcon = lipgloss.NewStyle().Foreground(charmtone.Iron).SetString("●") + s.ResourceBusyIcon = s.ResourceOfflineIcon.Foreground(charmtone.Citron) + s.ResourceErrorIcon = s.ResourceOfflineIcon.Foreground(charmtone.Coral) + s.ResourceOnlineIcon = s.ResourceOfflineIcon.Foreground(charmtone.Guac) + s.ResourceName = lipgloss.NewStyle().Foreground(charmtone.Squid) + s.ResourceStatus = lipgloss.NewStyle().Foreground(charmtone.Oyster) + s.ResourceAdditionalText = lipgloss.NewStyle().Foreground(charmtone.Oyster) // LSP s.LSP.ErrorDiagnostic = s.Base.Foreground(redDark)