@@ -0,0 +1,271 @@
1+// Package lsp provides a manager for Language Server Protocol (LSP) clients.
16+ "github.com/charmbracelet/crush/internal/config"
17+ "github.com/charmbracelet/crush/internal/csync"
18+ "github.com/charmbracelet/crush/internal/fsext"
19+ powernapconfig "github.com/charmbracelet/x/powernap/pkg/config"
20+ "github.com/charmbracelet/x/powernap/pkg/lsp"
21+ "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
22+ "github.com/sourcegraph/jsonrpc2"
25+// Manager handles lazy initialization of LSP clients based on file types.
27+ clients *csync.Map[string, *Client]
29+ manager *powernapconfig.Manager
30+ callback func(name string, client *Client)
34+// NewManager creates a new LSP manager service.
35+func NewManager(cfg *config.Config) *Manager {
36+ manager := powernapconfig.NewManager()
37+ manager.LoadDefaults()
39+ // Merge user-configured LSPs into the manager.
40+ for name, clientConfig := range cfg.LSP {
41+ if clientConfig.Disabled {
42+ slog.Debug("LSP disabled by user config", "name", name)
43+ manager.RemoveServer(name)
47+ // HACK: the user might have the command name in their config instead
48+ // of the actual name. Find and use the correct name.
49+ actualName := resolveServerName(manager, name)
50+ manager.AddServer(actualName, &powernapconfig.ServerConfig{
51+ Command: clientConfig.Command,
52+ Args: clientConfig.Args,
53+ Environment: clientConfig.Env,
54+ FileTypes: clientConfig.FileTypes,
55+ RootMarkers: clientConfig.RootMarkers,
56+ InitOptions: clientConfig.InitOptions,
57+ Settings: clientConfig.Options,
62+ clients: csync.NewMap[string, *Client](),
68+// Clients returns the map of LSP clients.
69+func (m *Manager) Clients() *csync.Map[string, *Client] {
73+// SetCallback sets a callback that is invoked when a new LSP
74+// client is successfully started. This allows the coordinator to add LSP tools.
75+func (s *Manager) SetCallback(cb func(name string, client *Client)) {
81+// Start starts an LSP server that can handle the given file path.
82+// If an appropriate LSP is already running, this is a no-op.
83+func (s *Manager) Start(ctx context.Context, filePath string) {
87+ var wg sync.WaitGroup
88+ for name, server := range s.manager.GetServers() {
89+ if !handles(server, filePath, s.cfg.WorkingDir()) {
93+ s.startServer(ctx, name, server)
99+// skipAutoStartCommands contains commands that are too generic or ambiguous to
100+// auto-start without explicit user configuration.
101+var skipAutoStartCommands = map[string]bool{
131+func (s *Manager) startServer(ctx context.Context, name string, server *powernapconfig.ServerConfig) {
132+ userConfigured := s.isUserConfigured(name)
134+ if !userConfigured {
135+ if _, err := exec.LookPath(server.Command); err != nil {
136+ slog.Debug("LSP server not installed, skipping", "name", name, "command", server.Command)
139+ if skipAutoStartCommands[server.Command] {
140+ slog.Debug("LSP command too generic for auto-start, skipping", "name", name, "command", server.Command)
145+ cfg := s.buildConfig(name, server)
146+ if client, ok := s.clients.Get(name); ok {
147+ switch client.GetServerState() {
148+ case StateReady, StateStarting:
149+ s.callback(name, client)
150+ // already done, return
154+ client, err := New(ctx, name, cfg, s.cfg.Resolver(), s.cfg.Options.DebugLSP)
156+ slog.Error("Failed to create LSP client", "name", name, "error", err)
159+ s.callback(name, client)
162+ s.clients.Set(name, client)
163+ s.callback(name, client)
166+ initCtx, cancel := context.WithTimeout(ctx, time.Duration(cmp.Or(cfg.Timeout, 30))*time.Second)
169+ if _, err := client.Initialize(initCtx, s.cfg.WorkingDir()); err != nil {
170+ slog.Error("LSP client initialization failed", "name", name, "error", err)
175+ if err := client.WaitForServerReady(initCtx); err != nil {
176+ slog.Warn("LSP server not fully ready, continuing anyway", "name", name, "error", err)
177+ client.SetServerState(StateError)
179+ client.SetServerState(StateReady)
182+ slog.Debug("LSP client started", "name", name)
185+func (s *Manager) isUserConfigured(name string) bool {
186+ cfg, ok := s.cfg.LSP[name]
187+ return ok && !cfg.Disabled
190+func (s *Manager) buildConfig(name string, server *powernapconfig.ServerConfig) config.LSPConfig {
191+ cfg := config.LSPConfig{
192+ Command: server.Command,
194+ Env: server.Environment,
195+ FileTypes: server.FileTypes,
196+ RootMarkers: server.RootMarkers,
197+ InitOptions: server.InitOptions,
198+ Options: server.Settings,
200+ if userCfg, ok := s.cfg.LSP[name]; ok {
201+ cfg.Timeout = userCfg.Timeout
206+func resolveServerName(manager *powernapconfig.Manager, name string) string {
207+ if _, ok := manager.GetServer(name); ok {
210+ for sname, server := range manager.GetServers() {
211+ if server.Command == name {
218+func handlesFiletype(server *powernapconfig.ServerConfig, ext string, language protocol.LanguageKind) bool {
219+ for _, ft := range server.FileTypes {
220+ if protocol.LanguageKind(ft) == language ||
221+ ft == strings.TrimPrefix(ext, ".") ||
229+func hasRootMarkers(dir string, markers []string) bool {
230+ if len(markers) == 0 {
233+ for _, pattern := range markers {
234+ // Use fsext.GlobWithDoubleStar to find matches
235+ matches, _, err := fsext.GlobWithDoubleStar(pattern, dir, 1)
236+ if err == nil && len(matches) > 0 {
243+func handles(server *powernapconfig.ServerConfig, filePath, workDir string) bool {
244+ language := lsp.DetectLanguage(filePath)
245+ ext := filepath.Ext(filePath)
246+ return handlesFiletype(server, ext, language) &&
247+ hasRootMarkers(workDir, server.RootMarkers)
250+// StopAll stops all running LSP clients and clears the client map.
251+func (s *Manager) StopAll(ctx context.Context) {
255+ var wg sync.WaitGroup
256+ for name, client := range s.clients.Seq2() {
258+ defer func() { s.callback(name, client) }()
259+ if err := client.Close(ctx); err != nil &&
260+ !errors.Is(err, io.EOF) &&
261+ !errors.Is(err, context.Canceled) &&
262+ !errors.Is(err, jsonrpc2.ErrClosed) &&
263+ err.Error() != "signal: killed" {
264+ slog.Warn("Failed to stop LSP client", "name", name, "error", err)
266+ client.SetServerState(StateStopped)
267+ slog.Debug("Stopped LSP client", "name", name)