lsp.go

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