1package app
  2
  3import (
  4	"context"
  5	"log/slog"
  6	"time"
  7
  8	"github.com/charmbracelet/crush/internal/config"
  9	"github.com/charmbracelet/crush/internal/log"
 10	"github.com/charmbracelet/crush/internal/lsp"
 11	"github.com/charmbracelet/crush/internal/lsp/watcher"
 12)
 13
 14// initLSPClients initializes LSP clients.
 15func (app *App) initLSPClients(ctx context.Context) {
 16	for name, clientConfig := range app.config.LSP {
 17		if clientConfig.Disabled {
 18			slog.Info("Skipping disabled LSP client", "name", name)
 19			continue
 20		}
 21		go app.createAndStartLSPClient(ctx, name, clientConfig)
 22	}
 23	slog.Info("LSP clients initialization started in background")
 24}
 25
 26// createAndStartLSPClient creates a new LSP client, initializes it, and starts its workspace watcher
 27func (app *App) createAndStartLSPClient(ctx context.Context, name string, config config.LSPConfig) {
 28	slog.Info("Creating LSP client", "name", name, "command", config.Command, "fileTypes", config.FileTypes, "args", config.Args)
 29
 30	// Check if any root markers exist in the working directory (config now has defaults)
 31	if !lsp.HasRootMarkers(app.config.WorkingDir(), config.RootMarkers) {
 32		slog.Info("Skipping LSP client - no root markers found", "name", name, "rootMarkers", config.RootMarkers)
 33		updateLSPState(name, lsp.StateDisabled, nil, nil, 0)
 34		return
 35	}
 36
 37	// Update state to starting
 38	updateLSPState(name, lsp.StateStarting, nil, nil, 0)
 39
 40	// Create LSP client.
 41	lspClient, err := lsp.New(ctx, name, config)
 42	if err != nil {
 43		slog.Error("Failed to create LSP client for", name, err)
 44		updateLSPState(name, lsp.StateError, err, nil, 0)
 45		return
 46	}
 47
 48	// Set diagnostics callback
 49	lspClient.SetDiagnosticsCallback(updateLSPDiagnostics)
 50
 51	// Increase initialization timeout as some servers take more time to start.
 52	initCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
 53	defer cancel()
 54
 55	// Initialize LSP client.
 56	_, err = lspClient.Initialize(initCtx, app.config.WorkingDir())
 57	if err != nil {
 58		slog.Error("Initialize failed", "name", name, "error", err)
 59		updateLSPState(name, lsp.StateError, err, lspClient, 0)
 60		lspClient.Close(ctx)
 61		return
 62	}
 63
 64	// Wait for the server to be ready.
 65	if err := lspClient.WaitForServerReady(initCtx); err != nil {
 66		slog.Error("Server failed to become ready", "name", name, "error", err)
 67		// Server never reached a ready state, but let's continue anyway, as
 68		// some functionality might still work.
 69		lspClient.SetServerState(lsp.StateError)
 70		updateLSPState(name, lsp.StateError, err, lspClient, 0)
 71	} else {
 72		// Server reached a ready state scuccessfully.
 73		slog.Info("LSP server is ready", "name", name)
 74		lspClient.SetServerState(lsp.StateReady)
 75		updateLSPState(name, lsp.StateReady, nil, lspClient, 0)
 76	}
 77
 78	slog.Info("LSP client initialized", "name", name)
 79
 80	// Create a child context that can be canceled when the app is shutting
 81	// down.
 82	watchCtx, cancelFunc := context.WithCancel(ctx)
 83
 84	// Create the workspace watcher.
 85	workspaceWatcher := watcher.New(name, lspClient)
 86
 87	// Store the cancel function to be called during cleanup.
 88	app.watcherCancelFuncs.Append(cancelFunc)
 89
 90	// Add to map with mutex protection before starting goroutine
 91	app.clientsMutex.Lock()
 92	app.LSPClients[name] = lspClient
 93	app.clientsMutex.Unlock()
 94
 95	// Run workspace watcher.
 96	app.lspWatcherWG.Add(1)
 97	go app.runWorkspaceWatcher(watchCtx, name, workspaceWatcher)
 98}
 99
100// runWorkspaceWatcher executes the workspace watcher for an LSP client.
101func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceWatcher *watcher.Client) {
102	defer app.lspWatcherWG.Done()
103	defer log.RecoverPanic("LSP-"+name, func() {
104		// Try to restart the client.
105		app.restartLSPClient(ctx, name)
106	})
107
108	workspaceWatcher.Watch(ctx, app.config.WorkingDir())
109	slog.Info("Workspace watcher stopped", "client", name)
110}
111
112// restartLSPClient attempts to restart a crashed or failed LSP client.
113func (app *App) restartLSPClient(ctx context.Context, name string) {
114	// Get the original configuration.
115	clientConfig, exists := app.config.LSP[name]
116	if !exists {
117		slog.Error("Cannot restart client, configuration not found", "client", name)
118		return
119	}
120
121	// Clean up the old client if it exists.
122	app.clientsMutex.Lock()
123	oldClient, exists := app.LSPClients[name]
124	if exists {
125		// Remove from map before potentially slow shutdown.
126		delete(app.LSPClients, name)
127	}
128	app.clientsMutex.Unlock()
129
130	if exists && oldClient != nil {
131		// Try to shut down client gracefully, but don't block on errors.
132		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
133		_ = oldClient.Close(shutdownCtx)
134		cancel()
135	}
136
137	// Create a new client using the shared function.
138	app.createAndStartLSPClient(ctx, name, clientConfig)
139	slog.Info("Successfully restarted LSP client", "client", name)
140}