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}