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