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