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