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}