1package lsp
2
3import (
4 "bufio"
5 "context"
6 "encoding/json"
7 "fmt"
8 "io"
9 "log/slog"
10 "strings"
11 "time"
12
13 "github.com/charmbracelet/crush/internal/config"
14)
15
16// WriteMessage writes an LSP message to the given writer
17func WriteMessage(w io.Writer, msg *Message) error {
18 data, err := json.Marshal(msg)
19 if err != nil {
20 return fmt.Errorf("failed to marshal message: %w", err)
21 }
22 cfg := config.Get()
23
24 if cfg.Options.DebugLSP {
25 slog.Debug("Sending message to server", "method", msg.Method, "id", msg.ID)
26 }
27
28 _, err = fmt.Fprintf(w, "Content-Length: %d\r\n\r\n", len(data))
29 if err != nil {
30 return fmt.Errorf("failed to write header: %w", err)
31 }
32
33 _, err = w.Write(data)
34 if err != nil {
35 return fmt.Errorf("failed to write message: %w", err)
36 }
37
38 return nil
39}
40
41// ReadMessage reads a single LSP message from the given reader
42func ReadMessage(r *bufio.Reader) (*Message, error) {
43 cfg := config.Get()
44 // Read headers
45 var contentLength int
46 for {
47 line, err := r.ReadString('\n')
48 if err != nil {
49 return nil, fmt.Errorf("failed to read header: %w", err)
50 }
51 line = strings.TrimSpace(line)
52
53 if cfg.Options.DebugLSP {
54 slog.Debug("Received header", "line", line)
55 }
56
57 if line == "" {
58 break // End of headers
59 }
60
61 if strings.HasPrefix(line, "Content-Length: ") {
62 _, err := fmt.Sscanf(line, "Content-Length: %d", &contentLength)
63 if err != nil {
64 return nil, fmt.Errorf("invalid Content-Length: %w", err)
65 }
66 }
67 }
68
69 if cfg.Options.DebugLSP {
70 slog.Debug("Content-Length", "length", contentLength)
71 }
72
73 // Read content
74 content := make([]byte, contentLength)
75 _, err := io.ReadFull(r, content)
76 if err != nil {
77 return nil, fmt.Errorf("failed to read content: %w", err)
78 }
79
80 if cfg.Options.DebugLSP {
81 slog.Debug("Received content", "content", string(content))
82 }
83
84 // Parse message
85 var msg Message
86 if err := json.Unmarshal(content, &msg); err != nil {
87 return nil, fmt.Errorf("failed to unmarshal message: %w", err)
88 }
89
90 return &msg, nil
91}
92
93// handleMessages reads and dispatches messages in a loop
94func (c *Client) handleMessages() {
95 cfg := config.Get()
96 for {
97 msg, err := ReadMessage(c.stdout)
98 if err != nil {
99 if cfg.Options.DebugLSP {
100 slog.Error("Error reading message", "error", err)
101 }
102 return
103 }
104
105 // Handle server->client request (has both Method and ID)
106 if msg.Method != "" && msg.ID != 0 {
107 if cfg.Options.DebugLSP {
108 slog.Debug("Received request from server", "method", msg.Method, "id", msg.ID)
109 }
110
111 response := &Message{
112 JSONRPC: "2.0",
113 ID: msg.ID,
114 }
115
116 // Look up handler for this method
117 c.serverHandlersMu.RLock()
118 handler, ok := c.serverRequestHandlers[msg.Method]
119 c.serverHandlersMu.RUnlock()
120
121 if ok {
122 result, err := handler(msg.Params)
123 if err != nil {
124 response.Error = &ResponseError{
125 Code: -32603,
126 Message: err.Error(),
127 }
128 } else {
129 rawJSON, err := json.Marshal(result)
130 if err != nil {
131 response.Error = &ResponseError{
132 Code: -32603,
133 Message: fmt.Sprintf("failed to marshal response: %v", err),
134 }
135 } else {
136 response.Result = rawJSON
137 }
138 }
139 } else {
140 response.Error = &ResponseError{
141 Code: -32601,
142 Message: fmt.Sprintf("method not found: %s", msg.Method),
143 }
144 }
145
146 // Send response back to server
147 if err := WriteMessage(c.stdin, response); err != nil {
148 slog.Error("Error sending response to server", "error", err)
149 }
150
151 continue
152 }
153
154 // Handle notification (has Method but no ID)
155 if msg.Method != "" && msg.ID == 0 {
156 c.notificationMu.RLock()
157 handler, ok := c.notificationHandlers[msg.Method]
158 c.notificationMu.RUnlock()
159
160 if ok {
161 if cfg.Options.DebugLSP {
162 slog.Debug("Handling notification", "method", msg.Method)
163 }
164 go handler(msg.Params)
165 } else if cfg.Options.DebugLSP {
166 slog.Debug("No handler for notification", "method", msg.Method)
167 }
168 continue
169 }
170
171 // Handle response to our request (has ID but no Method)
172 if msg.ID != 0 && msg.Method == "" {
173 c.handlersMu.RLock()
174 ch, ok := c.handlers[msg.ID]
175 c.handlersMu.RUnlock()
176
177 if ok {
178 if cfg.Options.DebugLSP {
179 slog.Debug("Received response for request", "id", msg.ID)
180 }
181 ch <- msg
182 close(ch)
183 } else if cfg.Options.DebugLSP {
184 slog.Debug("No handler for response", "id", msg.ID)
185 }
186 }
187 }
188}
189
190// Call makes a request and waits for the response with timeout
191func (c *Client) Call(ctx context.Context, method string, params any, result any) error {
192 cfg := config.Get()
193
194 // Add default timeout if context doesn't have one
195 if _, hasDeadline := ctx.Deadline(); !hasDeadline {
196 var cancel context.CancelFunc
197 ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
198 defer cancel()
199 }
200
201 id := c.nextID.Add(1)
202
203 if cfg.Options.DebugLSP {
204 slog.Debug("Making call", "method", method, "id", id)
205 }
206
207 msg, err := NewRequest(id, method, params)
208 if err != nil {
209 return fmt.Errorf("failed to create request: %w", err)
210 }
211
212 // Create response channel and track request
213 ch := make(chan *Message, 1)
214 c.handlersMu.Lock()
215 c.handlers[id] = ch
216 c.handlersMu.Unlock()
217
218 c.pendingRequestsMu.Lock()
219 c.pendingRequests[id] = time.Now()
220 c.pendingRequestsMu.Unlock()
221
222 defer func() {
223 c.handlersMu.Lock()
224 delete(c.handlers, id)
225 c.handlersMu.Unlock()
226
227 c.pendingRequestsMu.Lock()
228 delete(c.pendingRequests, id)
229 c.pendingRequestsMu.Unlock()
230 }()
231
232 // Send request
233 if err := WriteMessage(c.stdin, msg); err != nil {
234 return fmt.Errorf("failed to send request: %w", err)
235 }
236
237 if cfg.Options.DebugLSP {
238 slog.Debug("Request sent", "method", method, "id", id)
239 }
240
241 // Wait for response with context timeout
242 select {
243 case <-ctx.Done():
244 return fmt.Errorf("request timeout or cancelled for %s: %w", method, ctx.Err())
245 case resp := <-ch:
246 if cfg.Options.DebugLSP {
247 slog.Debug("Received response", "id", id)
248 }
249
250 if resp.Error != nil {
251 return fmt.Errorf("request failed: %s (code: %d)", resp.Error.Message, resp.Error.Code)
252 }
253
254 if result != nil {
255 // If result is a json.RawMessage, just copy the raw bytes
256 if rawMsg, ok := result.(*json.RawMessage); ok {
257 *rawMsg = resp.Result
258 return nil
259 }
260 // Otherwise unmarshal into the provided type
261 if err := json.Unmarshal(resp.Result, result); err != nil {
262 return fmt.Errorf("failed to unmarshal result: %w", err)
263 }
264 }
265
266 return nil
267 }
268}
269
270// Notify sends a notification (a request without an ID that doesn't expect a response)
271func (c *Client) Notify(ctx context.Context, method string, params any) error {
272 cfg := config.Get()
273 if cfg.Options.DebugLSP {
274 slog.Debug("Sending notification", "method", method)
275 }
276
277 msg, err := NewNotification(method, params)
278 if err != nil {
279 return fmt.Errorf("failed to create notification: %w", err)
280 }
281
282 if err := WriteMessage(c.stdin, msg); err != nil {
283 return fmt.Errorf("failed to send notification: %w", err)
284 }
285
286 return nil
287}
288
289type (
290 NotificationHandler func(params json.RawMessage)
291 ServerRequestHandler func(params json.RawMessage) (any, error)
292)