lsp.go

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