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