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