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