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