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 the workspace watcher
63 workspaceWatcher := watcher.NewWorkspaceWatcher(name, lspClient)
64
65 // Store the cancel function to be called during cleanup
66 app.cancelFuncsMutex.Lock()
67 app.watcherCancelFuncs = append(app.watcherCancelFuncs, cancelFunc)
68 app.cancelFuncsMutex.Unlock()
69
70 // Add the watcher to a WaitGroup to track active goroutines
71 app.lspWatcherWG.Add(1)
72
73 // Add to map with mutex protection before starting goroutine
74 app.clientsMutex.Lock()
75 app.LSPClients[name] = lspClient
76 app.clientsMutex.Unlock()
77
78 go app.runWorkspaceWatcher(watchCtx, name, workspaceWatcher)
79}
80
81// runWorkspaceWatcher executes the workspace watcher for an LSP client
82func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceWatcher *watcher.WorkspaceWatcher) {
83 defer app.lspWatcherWG.Done()
84 defer log.RecoverPanic("LSP-"+name, func() {
85 // Try to restart the client
86 app.restartLSPClient(ctx, name)
87 })
88
89 workspaceWatcher.WatchWorkspace(ctx, app.config.WorkingDir())
90 slog.Info("Workspace watcher stopped", "client", name)
91}
92
93// restartLSPClient attempts to restart a crashed or failed LSP client
94func (app *App) restartLSPClient(ctx context.Context, name string) {
95 // Get the original configuration
96 clientConfig, exists := app.config.LSP[name]
97 if !exists {
98 slog.Error("Cannot restart client, configuration not found", "client", name)
99 return
100 }
101
102 // Clean up the old client if it exists
103 app.clientsMutex.Lock()
104 oldClient, exists := app.LSPClients[name]
105 if exists {
106 delete(app.LSPClients, name) // Remove from map before potentially slow shutdown
107 }
108 app.clientsMutex.Unlock()
109
110 if exists && oldClient != nil {
111 // Try to shut it down gracefully, but don't block on errors
112 shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
113 _ = oldClient.Shutdown(shutdownCtx)
114 cancel()
115 }
116
117 // Create a new client using the shared function
118 app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
119 slog.Info("Successfully restarted LSP client", "client", name)
120}