lsp.go

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