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