on main help sign in
feat(lsp): show user-configured LSPs in the UI (#2192)
5179b28Carlos Alexandro Becker
internal/app/app.go+5
@@ -126,9 +126,14 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
126
126
127 // Set up callback for LSP state updates.
127 // Set up callback for LSP state updates.
128 app.LSPManager.SetCallback(func(name string, client *lsp.Client) {
128 app.LSPManager.SetCallback(func(name string, client *lsp.Client) {
129+ if client == nil {
130+ updateLSPState(name, lsp.StateUnstarted, nil, nil, 0)
131+ return
132+ }
129 client.SetDiagnosticsCallback(updateLSPDiagnostics)
133 client.SetDiagnosticsCallback(updateLSPDiagnostics)
130 updateLSPState(name, client.GetServerState(), nil, client, 0)
134 updateLSPState(name, client.GetServerState(), nil, client, 0)
131 })
135 })
136+ go app.LSPManager.TrackConfigured()
132
137
133 return app, nil
138 return app, nil
134}
139}
internal/lsp/client.go+2−1
@@ -241,10 +241,11 @@ func (c *Client) Restart() error {
241type ServerState int
241type ServerState int
242
242
243const (
243const (
244− StateStopped ServerState = iota
244+ StateUnstarted ServerState = iota
245 StateStarting
245 StateStarting
246 StateReady
246 StateReady
247 StateError
247 StateError
248+ StateStopped
248 StateDisabled
249 StateDisabled
249)
250)
250
251
internal/lsp/manager.go+18
@@ -77,6 +77,24 @@ func (s *Manager) SetCallback(cb func(name string, client *Client)) {
77 s.callback = cb
77 s.callback = cb
78}
78}
79
79
80+// TrackConfigured will callback the user-configured LSPs, but will not create
81+// any clients.
82+func (s *Manager) TrackConfigured() {
83+ s.mu.Lock()
84+ defer s.mu.Unlock()
85+
86+ var wg sync.WaitGroup
87+ for name := range s.manager.GetServers() {
88+ if !s.isUserConfigured(name) {
89+ continue
90+ }
91+ wg.Go(func() {
92+ s.callback(name, nil)
93+ })
94+ }
95+ wg.Wait()
96+}
97+
80// Start starts an LSP server that can handle the given file path.
98// Start starts an LSP server that can handle the given file path.
81// If an appropriate LSP is already running, this is a no-op.
99// If an appropriate LSP is already running, this is a no-op.
82func (s *Manager) Start(ctx context.Context, path string) {
100func (s *Manager) Start(ctx context.Context, path string) {
internal/ui/model/lsp.go+25−25
@@ -31,26 +31,23 @@ func (m *UI) lspInfo(width, maxItems int, isSection bool) string {
31
31
32 var lsps []LSPInfo
32 var lsps []LSPInfo
33 for _, state := range states {
33 for _, state := range states {
34+ lspErrs := map[protocol.DiagnosticSeverity]int{}
34− client, ok := m.com.App.LSPManager.Clients().Get(state.Name)
35+ if client, ok := m.com.App.LSPManager.Clients().Get(state.Name); ok {
35− if !ok {
36− continue
37− }
38− counts := client.GetDiagnosticCounts()
36+ counts := client.GetDiagnosticCounts()
39− lspErrs := map[protocol.DiagnosticSeverity]int{
40− protocol.SeverityError: counts.Error,
37+ lspErrs[protocol.SeverityError] = counts.Error
41− protocol.SeverityWarning: counts.Warning,
38+ lspErrs[protocol.SeverityWarning] = counts.Warning
42− protocol.SeverityHint: counts.Hint,
39+ lspErrs[protocol.SeverityHint] = counts.Hint
43− protocol.SeverityInformation: counts.Information,
40+ lspErrs[protocol.SeverityInformation] = counts.Information
44 }
41 }
45
42
46 lsps = append(lsps, LSPInfo{LSPClientInfo: state, Diagnostics: lspErrs})
43 lsps = append(lsps, LSPInfo{LSPClientInfo: state, Diagnostics: lspErrs})
47 }
44 }
48
45
49− title := t.Subtle.Render("LSPs")
46+ title := t.ResourceGroupTitle.Render("LSPs")
50 if isSection {
47 if isSection {
51 title = common.Section(t, title, width)
48 title = common.Section(t, title, width)
52 }
49 }
53− list := t.Subtle.Render("None")
50+ list := t.ResourceAdditionalText.Render("None")
54 if len(lsps) > 0 {
51 if len(lsps) > 0 {
55 list = lspList(t, lsps, width, maxItems)
52 list = lspList(t, lsps, width, maxItems)
56 }
53 }
@@ -85,30 +82,33 @@ func lspList(t *styles.Styles, lsps []LSPInfo, width, maxItems int) string {
85 var renderedLsps []string
82 var renderedLsps []string
86 for _, l := range lsps {
83 for _, l := range lsps {
87 var icon string
84 var icon string
88− title := l.Name
85+ title := t.ResourceName.Render(l.Name)
89 var description string
86 var description string
90 var diagnostics string
87 var diagnostics string
91 switch l.State {
88 switch l.State {
89+ case lsp.StateUnstarted:
90+ icon = t.ResourceOfflineIcon.String()
91+ description = t.ResourceStatus.Render("unstarted")
92 case lsp.StateStopped:
92 case lsp.StateStopped:
93− icon = t.ItemOfflineIcon.Foreground(t.Muted.GetBackground()).String()
93+ icon = t.ResourceOfflineIcon.String()
94− description = t.Subtle.Render("stopped")
94+ description = t.ResourceStatus.Render("stopped")
95 case lsp.StateStarting:
95 case lsp.StateStarting:
96− icon = t.ItemBusyIcon.String()
96+ icon = t.ResourceBusyIcon.String()
97− description = t.Subtle.Render("starting...")
97+ description = t.ResourceStatus.Render("starting...")
98 case lsp.StateReady:
98 case lsp.StateReady:
99− icon = t.ItemOnlineIcon.String()
99+ icon = t.ResourceOnlineIcon.String()
100 diagnostics = lspDiagnostics(t, l.Diagnostics)
100 diagnostics = lspDiagnostics(t, l.Diagnostics)
101 case lsp.StateError:
101 case lsp.StateError:
102− icon = t.ItemErrorIcon.String()
102+ icon = t.ResourceErrorIcon.String()
103− description = t.Subtle.Render("error")
103+ description = t.ResourceStatus.Render("error")
104 if l.Error != nil {
104 if l.Error != nil {
105− description = t.Subtle.Render(fmt.Sprintf("error: %s", l.Error.Error()))
105+ description = t.ResourceStatus.Render(fmt.Sprintf("error: %s", l.Error.Error()))
106 }
106 }
107 case lsp.StateDisabled:
107 case lsp.StateDisabled:
108− icon = t.ItemOfflineIcon.Foreground(t.Muted.GetBackground()).String()
108+ icon = t.ResourceOfflineIcon.Foreground(t.Muted.GetBackground()).String()
109− description = t.Subtle.Render("disabled")
109+ description = t.ResourceStatus.Render("disabled")
110 default:
110 default:
111− icon = t.ItemOfflineIcon.String()
111+ icon = t.ResourceOfflineIcon.String()
112 }
112 }
113 renderedLsps = append(renderedLsps, common.Status(t, common.StatusOpts{
113 renderedLsps = append(renderedLsps, common.Status(t, common.StatusOpts{
114 Icon: icon,
114 Icon: icon,
@@ -121,7 +121,7 @@ func lspList(t *styles.Styles, lsps []LSPInfo, width, maxItems int) string {
121 if len(renderedLsps) > maxItems {
121 if len(renderedLsps) > maxItems {
122 visibleItems := renderedLsps[:maxItems-1]
122 visibleItems := renderedLsps[:maxItems-1]
123 remaining := len(renderedLsps) - maxItems
123 remaining := len(renderedLsps) - maxItems
124− visibleItems = append(visibleItems, t.Subtle.Render(fmt.Sprintf("…and %d more", remaining)))
124+ visibleItems = append(visibleItems, t.ResourceAdditionalText.Render(fmt.Sprintf("…and %d more", remaining)))
125 return lipgloss.JoinVertical(lipgloss.Left, visibleItems...)
125 return lipgloss.JoinVertical(lipgloss.Left, visibleItems...)
126 }
126 }
127 return lipgloss.JoinVertical(lipgloss.Left, renderedLsps...)
127 return lipgloss.JoinVertical(lipgloss.Left, renderedLsps...)
internal/ui/model/mcp.go+13−13
@@ -22,11 +22,11 @@ func (m *UI) mcpInfo(width, maxItems int, isSection bool) string {
22 }
22 }
23 }
23 }
24
24
25− title := t.Subtle.Render("MCPs")
25+ title := t.ResourceGroupTitle.Render("MCPs")
26 if isSection {
26 if isSection {
27 title = common.Section(t, title, width)
27 title = common.Section(t, title, width)
28 }
28 }
29− list := t.Subtle.Render("None")
29+ list := t.ResourceAdditionalText.Render("None")
30 if len(mcps) > 0 {
30 if len(mcps) > 0 {
31 list = mcpList(t, mcps, width, maxItems)
31 list = mcpList(t, mcps, width, maxItems)
32 }
32 }
@@ -59,28 +59,28 @@ func mcpList(t *styles.Styles, mcps []mcp.ClientInfo, width, maxItems int) strin
59
59
60 for _, m := range mcps {
60 for _, m := range mcps {
61 var icon string
61 var icon string
62− title := m.Name
62+ title := t.ResourceName.Render(m.Name)
63 var description string
63 var description string
64 var extraContent string
64 var extraContent string
65
65
66 switch m.State {
66 switch m.State {
67 case mcp.StateStarting:
67 case mcp.StateStarting:
68− icon = t.ItemBusyIcon.String()
68+ icon = t.ResourceBusyIcon.String()
69− description = t.Subtle.Render("starting...")
69+ description = t.ResourceStatus.Render("starting...")
70 case mcp.StateConnected:
70 case mcp.StateConnected:
71− icon = t.ItemOnlineIcon.String()
71+ icon = t.ResourceOnlineIcon.String()
72 extraContent = mcpCounts(t, m.Counts)
72 extraContent = mcpCounts(t, m.Counts)
73 case mcp.StateError:
73 case mcp.StateError:
74− icon = t.ItemErrorIcon.String()
74+ icon = t.ResourceErrorIcon.String()
75− description = t.Subtle.Render("error")
75+ description = t.ResourceStatus.Render("error")
76 if m.Error != nil {
76 if m.Error != nil {
77− description = t.Subtle.Render(fmt.Sprintf("error: %s", m.Error.Error()))
77+ description = t.ResourceStatus.Render(fmt.Sprintf("error: %s", m.Error.Error()))
78 }
78 }
79 case mcp.StateDisabled:
79 case mcp.StateDisabled:
80− icon = t.ItemOfflineIcon.Foreground(t.Muted.GetBackground()).String()
80+ icon = t.ResourceOfflineIcon.Foreground(t.Muted.GetBackground()).String()
81− description = t.Subtle.Render("disabled")
81+ description = t.ResourceStatus.Render("disabled")
82 default:
82 default:
83− icon = t.ItemOfflineIcon.String()
83+ icon = t.ResourceOfflineIcon.String()
84 }
84 }
85
85
86 renderedMcps = append(renderedMcps, common.Status(t, common.StatusOpts{
86 renderedMcps = append(renderedMcps, common.Status(t, common.StatusOpts{
@@ -94,7 +94,7 @@ func mcpList(t *styles.Styles, mcps []mcp.ClientInfo, width, maxItems int) strin
94 if len(renderedMcps) > maxItems {
94 if len(renderedMcps) > maxItems {
95 visibleItems := renderedMcps[:maxItems-1]
95 visibleItems := renderedMcps[:maxItems-1]
96 remaining := len(renderedMcps) - maxItems
96 remaining := len(renderedMcps) - maxItems
97− visibleItems = append(visibleItems, t.Subtle.Render(fmt.Sprintf("…and %d more", remaining)))
97+ visibleItems = append(visibleItems, t.ResourceAdditionalText.Render(fmt.Sprintf("…and %d more", remaining)))
98 return lipgloss.JoinVertical(lipgloss.Left, visibleItems...)
98 return lipgloss.JoinVertical(lipgloss.Left, visibleItems...)
99 }
99 }
100 return lipgloss.JoinVertical(lipgloss.Left, renderedMcps...)
100 return lipgloss.JoinVertical(lipgloss.Left, renderedMcps...)
internal/ui/styles/styles.go+16−8
@@ -109,10 +109,14 @@ type Styles struct {
109 TextSelection lipgloss.Style
109 TextSelection lipgloss.Style
110
110
111 // LSP and MCP status indicators
111 // LSP and MCP status indicators
112− ItemOfflineIcon lipgloss.Style
112+ ResourceGroupTitle lipgloss.Style
113− ItemBusyIcon lipgloss.Style
113+ ResourceOfflineIcon lipgloss.Style
114− ItemErrorIcon lipgloss.Style
114+ ResourceBusyIcon lipgloss.Style
115+ ResourceErrorIcon lipgloss.Style
116+ ResourceOnlineIcon lipgloss.Style
117+ ResourceName lipgloss.Style
118+ ResourceStatus lipgloss.Style
115− ItemOnlineIcon lipgloss.Style
119+ ResourceAdditionalText lipgloss.Style
116
120
117 // Markdown & Chroma
121 // Markdown & Chroma
118 Markdown ansi.StyleConfig
122 Markdown ansi.StyleConfig
@@ -1199,10 +1203,14 @@ func DefaultStyles() Styles {
1199 s.Initialize.Accent = s.Base.Foreground(greenDark)
1203 s.Initialize.Accent = s.Base.Foreground(greenDark)
1200
1204
1201 // LSP and MCP status.
1205 // LSP and MCP status.
1206+ s.ResourceGroupTitle = lipgloss.NewStyle().Foreground(charmtone.Oyster)
1202− s.ItemOfflineIcon = lipgloss.NewStyle().Foreground(charmtone.Squid).SetString("●")
1207+ s.ResourceOfflineIcon = lipgloss.NewStyle().Foreground(charmtone.Iron).SetString("●")
1203− s.ItemBusyIcon = s.ItemOfflineIcon.Foreground(charmtone.Citron)
1208+ s.ResourceBusyIcon = s.ResourceOfflineIcon.Foreground(charmtone.Citron)
1204− s.ItemErrorIcon = s.ItemOfflineIcon.Foreground(charmtone.Coral)
1209+ s.ResourceErrorIcon = s.ResourceOfflineIcon.Foreground(charmtone.Coral)
1205− s.ItemOnlineIcon = s.ItemOfflineIcon.Foreground(charmtone.Guac)
1210+ s.ResourceOnlineIcon = s.ResourceOfflineIcon.Foreground(charmtone.Guac)
1211+ s.ResourceName = lipgloss.NewStyle().Foreground(charmtone.Squid)
1212+ s.ResourceStatus = lipgloss.NewStyle().Foreground(charmtone.Oyster)
1213+ s.ResourceAdditionalText = lipgloss.NewStyle().Foreground(charmtone.Oyster)
1206
1214
1207 // LSP
1215 // LSP
1208 s.LSP.ErrorDiagnostic = s.Base.Foreground(redDark)
1216 s.LSP.ErrorDiagnostic = s.Base.Foreground(redDark)