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}