lsp.go

  1package app
  2
  3import (
  4	"context"
  5	"log/slog"
  6	"os/exec"
  7	"slices"
  8	"sync"
  9	"time"
 10
 11	"github.com/charmbracelet/crush/internal/config"
 12	"github.com/charmbracelet/crush/internal/lsp"
 13	powernapconfig "github.com/charmbracelet/x/powernap/pkg/config"
 14)
 15
 16// initLSPClients initializes LSP clients.
 17func (app *App) initLSPClients(ctx context.Context) {
 18	slog.Info("LSP clients initialization started")
 19
 20	manager := powernapconfig.NewManager()
 21	manager.LoadDefaults()
 22
 23	var userConfiguredLSPs []string
 24	for name, clientConfig := range app.config.LSP {
 25		if clientConfig.Disabled {
 26			slog.Info("Skipping disabled LSP client", "name", name)
 27			manager.RemoveServer(name)
 28			continue
 29		}
 30
 31		// HACK: the user might have the command name in their config, instead
 32		// of the actual name. This finds out these cases, and adjusts the name
 33		// accordingly.
 34		if _, ok := manager.GetServer(name); !ok {
 35			for sname, server := range manager.GetServers() {
 36				if server.Command == name {
 37					name = sname
 38					break
 39				}
 40			}
 41		}
 42		userConfiguredLSPs = append(userConfiguredLSPs, name)
 43		manager.AddServer(name, &powernapconfig.ServerConfig{
 44			Command:     clientConfig.Command,
 45			Args:        clientConfig.Args,
 46			Environment: clientConfig.Env,
 47			FileTypes:   clientConfig.FileTypes,
 48			RootMarkers: clientConfig.RootMarkers,
 49			InitOptions: clientConfig.InitOptions,
 50			Settings:    clientConfig.Options,
 51		})
 52	}
 53
 54	servers := manager.GetServers()
 55	filtered := lsp.FilterMatching(app.config.WorkingDir(), servers)
 56
 57	for _, name := range userConfiguredLSPs {
 58		if _, ok := filtered[name]; !ok {
 59			updateLSPState(name, lsp.StateDisabled, nil, nil, 0)
 60		}
 61	}
 62
 63	var wg sync.WaitGroup
 64	for name, server := range filtered {
 65		if app.config.Options.AutoLSP != nil && !*app.config.Options.AutoLSP && !slices.Contains(userConfiguredLSPs, name) {
 66			slog.Debug("Ignoring non user-define LSP client due to AutoLSP being disabled", "name", name)
 67			continue
 68		}
 69		wg.Go(func() {
 70			app.createAndStartLSPClient(
 71				ctx, name,
 72				toOurConfig(server),
 73				slices.Contains(userConfiguredLSPs, name),
 74			)
 75		})
 76	}
 77	wg.Wait()
 78
 79	if err := app.AgentCoordinator.UpdateModels(ctx); err != nil {
 80		slog.Error("Failed to refresh tools after LSP startup", "error", err)
 81	}
 82}
 83
 84func toOurConfig(in *powernapconfig.ServerConfig) config.LSPConfig {
 85	return config.LSPConfig{
 86		Command:     in.Command,
 87		Args:        in.Args,
 88		Env:         in.Environment,
 89		FileTypes:   in.FileTypes,
 90		RootMarkers: in.RootMarkers,
 91		InitOptions: in.InitOptions,
 92		Options:     in.Settings,
 93	}
 94}
 95
 96// createAndStartLSPClient creates a new LSP client, initializes it, and starts its workspace watcher.
 97func (app *App) createAndStartLSPClient(ctx context.Context, name string, config config.LSPConfig, userConfigured bool) {
 98	if !userConfigured {
 99		if _, err := exec.LookPath(config.Command); err != nil {
100			slog.Warn("Default LSP config skipped: server not installed", "name", name, "error", err)
101			return
102		}
103	}
104
105	slog.Debug("Creating LSP client", "name", name, "command", config.Command, "fileTypes", config.FileTypes, "args", config.Args)
106
107	// Update state to starting.
108	updateLSPState(name, lsp.StateStarting, nil, nil, 0)
109
110	// Create LSP client.
111	lspClient, err := lsp.New(ctx, name, config, app.config.Resolver())
112	if err != nil {
113		if !userConfigured {
114			slog.Warn("Default LSP config skipped due to error", "name", name, "error", err)
115			updateLSPState(name, lsp.StateDisabled, nil, nil, 0)
116			return
117		}
118		slog.Error("Failed to create LSP client for", "name", name, "error", err)
119		updateLSPState(name, lsp.StateError, err, nil, 0)
120		return
121	}
122
123	// Set diagnostics callback
124	lspClient.SetDiagnosticsCallback(updateLSPDiagnostics)
125
126	// Increase initialization timeout as some servers take more time to start.
127	initCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
128	defer cancel()
129
130	// Initialize LSP client.
131	_, err = lspClient.Initialize(initCtx, app.config.WorkingDir())
132	if err != nil {
133		slog.Error("LSP client initialization failed", "name", name, "error", err)
134		updateLSPState(name, lsp.StateError, err, lspClient, 0)
135		lspClient.Close(ctx)
136		return
137	}
138
139	// Wait for the server to be ready.
140	if err := lspClient.WaitForServerReady(initCtx); err != nil {
141		slog.Error("Server failed to become ready", "name", name, "error", err)
142		// Server never reached a ready state, but let's continue anyway, as
143		// some functionality might still work.
144		lspClient.SetServerState(lsp.StateError)
145		updateLSPState(name, lsp.StateError, err, lspClient, 0)
146	} else {
147		// Server reached a ready state successfully.
148		slog.Debug("LSP server is ready", "name", name)
149		lspClient.SetServerState(lsp.StateReady)
150		updateLSPState(name, lsp.StateReady, nil, lspClient, 0)
151	}
152
153	slog.Debug("LSP client initialized", "name", name)
154
155	// Add to map with mutex protection before starting goroutine
156	app.LSPClients.Set(name, lspClient)
157}