lsp.go

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