lsp.go

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