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.watcherCancelFuncs.Append(cancelFunc)
67
68 // Add to map with mutex protection before starting goroutine
69 app.clientsMutex.Lock()
70 app.LSPClients[name] = lspClient
71 app.clientsMutex.Unlock()
72
73 // Run workspace watcher.
74 app.lspWatcherWG.Add(1)
75 go app.runWorkspaceWatcher(watchCtx, name, workspaceWatcher)
76}
77
78// runWorkspaceWatcher executes the workspace watcher for an LSP client.
79func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceWatcher *watcher.WorkspaceWatcher) {
80 defer app.lspWatcherWG.Done()
81 defer log.RecoverPanic("LSP-"+name, func() {
82 // Try to restart the client.
83 app.restartLSPClient(ctx, name)
84 })
85
86 workspaceWatcher.WatchWorkspace(ctx, app.config.WorkingDir())
87 slog.Info("Workspace watcher stopped", "client", name)
88}
89
90// restartLSPClient attempts to restart a crashed or failed LSP client.
91func (app *App) restartLSPClient(ctx context.Context, name string) {
92 // Get the original configuration.
93 clientConfig, exists := app.config.LSP[name]
94 if !exists {
95 slog.Error("Cannot restart client, configuration not found", "client", name)
96 return
97 }
98
99 // Clean up the old client if it exists.
100 app.clientsMutex.Lock()
101 oldClient, exists := app.LSPClients[name]
102 if exists {
103 // Remove from map before potentially slow shutdown.
104 delete(app.LSPClients, name)
105 }
106 app.clientsMutex.Unlock()
107
108 if exists && oldClient != nil {
109 // Try to shut down client gracefully, but don't block on errors.
110 shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
111 _ = oldClient.Shutdown(shutdownCtx)
112 cancel()
113 }
114
115 // Create a new client using the shared function.
116 app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
117 slog.Info("Successfully restarted LSP client", "client", name)
118}