1package app
2
3import (
4 "context"
5 "log/slog"
6 "os/exec"
7 "slices"
8 "sync"
9 "time"
10
11 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/lsp"
13 powernapconfig "github.com/charmbracelet/x/powernap/pkg/config"
14)
15
16// initLSPClients initializes LSP clients.
17func (app *App) initLSPClients(ctx context.Context) {
18 slog.Info("LSP clients initialization started")
19
20 manager := powernapconfig.NewManager()
21 manager.LoadDefaults()
22
23 var userConfiguredLSPs []string
24 for name, clientConfig := range app.config.LSP {
25 if clientConfig.Disabled {
26 slog.Info("Skipping disabled LSP client", "name", name)
27 manager.RemoveServer(name)
28 continue
29 }
30
31 // HACK: the user might have the command name in their config, instead
32 // of the actual name. This finds out these cases, and adjusts the name
33 // accordingly.
34 if _, ok := manager.GetServer(name); !ok {
35 for sname, server := range manager.GetServers() {
36 if server.Command == name {
37 name = sname
38 break
39 }
40 }
41 }
42 userConfiguredLSPs = append(userConfiguredLSPs, name)
43 manager.AddServer(name, &powernapconfig.ServerConfig{
44 Command: clientConfig.Command,
45 Args: clientConfig.Args,
46 Environment: clientConfig.Env,
47 FileTypes: clientConfig.FileTypes,
48 RootMarkers: clientConfig.RootMarkers,
49 InitOptions: clientConfig.InitOptions,
50 Settings: clientConfig.Options,
51 })
52 }
53
54 servers := manager.GetServers()
55 filtered := lsp.FilterMatching(app.config.WorkingDir(), servers)
56
57 for _, name := range userConfiguredLSPs {
58 if _, ok := filtered[name]; !ok {
59 updateLSPState(name, lsp.StateDisabled, nil, nil, 0)
60 }
61 }
62
63 var wg sync.WaitGroup
64 for name, server := range filtered {
65 if app.config.Options.AutoLSP != nil && !*app.config.Options.AutoLSP && !slices.Contains(userConfiguredLSPs, name) {
66 slog.Debug("Ignoring non user-define LSP client due to AutoLSP being disabled", "name", name)
67 continue
68 }
69 wg.Go(func() {
70 app.createAndStartLSPClient(
71 ctx, name,
72 toOurConfig(server),
73 slices.Contains(userConfiguredLSPs, name),
74 )
75 })
76 }
77 wg.Wait()
78
79 if err := app.AgentCoordinator.UpdateModels(ctx); err != nil {
80 slog.Error("Failed to refresh tools after LSP startup", "error", err)
81 }
82}
83
84func toOurConfig(in *powernapconfig.ServerConfig) config.LSPConfig {
85 return config.LSPConfig{
86 Command: in.Command,
87 Args: in.Args,
88 Env: in.Environment,
89 FileTypes: in.FileTypes,
90 RootMarkers: in.RootMarkers,
91 InitOptions: in.InitOptions,
92 Options: in.Settings,
93 }
94}
95
96// createAndStartLSPClient creates a new LSP client, initializes it, and starts its workspace watcher.
97func (app *App) createAndStartLSPClient(ctx context.Context, name string, config config.LSPConfig, userConfigured bool) {
98 if !userConfigured {
99 if _, err := exec.LookPath(config.Command); err != nil {
100 slog.Warn("Default LSP config skipped: server not installed", "name", name, "error", err)
101 return
102 }
103 }
104
105 slog.Debug("Creating LSP client", "name", name, "command", config.Command, "fileTypes", config.FileTypes, "args", config.Args)
106
107 // Update state to starting.
108 updateLSPState(name, lsp.StateStarting, nil, nil, 0)
109
110 // Create LSP client.
111 lspClient, err := lsp.New(ctx, name, config, app.config.Resolver())
112 if err != nil {
113 if !userConfigured {
114 slog.Warn("Default LSP config skipped due to error", "name", name, "error", err)
115 updateLSPState(name, lsp.StateDisabled, nil, nil, 0)
116 return
117 }
118 slog.Error("Failed to create LSP client for", "name", name, "error", err)
119 updateLSPState(name, lsp.StateError, err, nil, 0)
120 return
121 }
122
123 // Set diagnostics callback
124 lspClient.SetDiagnosticsCallback(updateLSPDiagnostics)
125
126 // Increase initialization timeout as some servers take more time to start.
127 initCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
128 defer cancel()
129
130 // Initialize LSP client.
131 _, err = lspClient.Initialize(initCtx, app.config.WorkingDir())
132 if err != nil {
133 slog.Error("LSP client initialization failed", "name", name, "error", err)
134 updateLSPState(name, lsp.StateError, err, lspClient, 0)
135 lspClient.Close(ctx)
136 return
137 }
138
139 // Wait for the server to be ready.
140 if err := lspClient.WaitForServerReady(initCtx); err != nil {
141 slog.Error("Server failed to become ready", "name", name, "error", err)
142 // Server never reached a ready state, but let's continue anyway, as
143 // some functionality might still work.
144 lspClient.SetServerState(lsp.StateError)
145 updateLSPState(name, lsp.StateError, err, lspClient, 0)
146 } else {
147 // Server reached a ready state successfully.
148 slog.Debug("LSP server is ready", "name", name)
149 lspClient.SetServerState(lsp.StateReady)
150 updateLSPState(name, lsp.StateReady, nil, lspClient, 0)
151 }
152
153 slog.Debug("LSP client initialized", "name", name)
154
155 // Add to map with mutex protection before starting goroutine
156 app.LSPClients.Set(name, lspClient)
157}